Merge branch 'develop' into feature/database-compaction
This commit is contained in:
commit
627e5a0a49
1271 changed files with 42114 additions and 70683 deletions
|
|
@ -1,12 +1,26 @@
|
|||
# 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.ActivityPub do
|
||||
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Upload
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.WebFinger
|
||||
|
||||
import Ecto.Query
|
||||
import Pleroma.Web.ActivityPub.Utils
|
||||
import Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
require Logger
|
||||
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
|
|
@ -16,23 +30,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp get_recipients(%{"type" => "Announce"} = data) do
|
||||
to = data["to"] || []
|
||||
cc = data["cc"] || []
|
||||
recipients = to ++ cc
|
||||
actor = User.get_cached_by_ap_id(data["actor"])
|
||||
|
||||
recipients
|
||||
|> Enum.filter(fn recipient ->
|
||||
case User.get_cached_by_ap_id(recipient) do
|
||||
nil ->
|
||||
true
|
||||
recipients =
|
||||
(to ++ cc)
|
||||
|> Enum.filter(fn recipient ->
|
||||
case User.get_cached_by_ap_id(recipient) do
|
||||
nil ->
|
||||
true
|
||||
|
||||
user ->
|
||||
User.following?(user, actor)
|
||||
end
|
||||
end)
|
||||
user ->
|
||||
User.following?(user, actor)
|
||||
end
|
||||
end)
|
||||
|
||||
{recipients, to, cc}
|
||||
end
|
||||
|
||||
defp get_recipients(%{"type" => "Create"} = data) do
|
||||
to = data["to"] || []
|
||||
cc = data["cc"] || []
|
||||
actor = data["actor"] || []
|
||||
recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
|
||||
{recipients, to, cc}
|
||||
end
|
||||
|
||||
defp get_recipients(data) do
|
||||
to = data["to"] || []
|
||||
cc = data["cc"] || []
|
||||
|
|
@ -53,14 +75,53 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def insert(map, local \\ true) when is_map(map) do
|
||||
with nil <- Activity.normalize(map),
|
||||
map <- lazy_put_activity_defaults(map),
|
||||
:ok <- check_actor_is_active(map["actor"]),
|
||||
{:ok, map} <- MRF.filter(map),
|
||||
{:ok, map} <- insert_full_object(map) do
|
||||
{recipients, _, _} = get_recipients(map)
|
||||
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
|
||||
limit = Pleroma.Config.get([:instance, :remote_limit])
|
||||
String.length(content) <= limit
|
||||
end
|
||||
|
||||
defp check_remote_limit(_), do: true
|
||||
|
||||
def increase_note_count_if_public(actor, object) do
|
||||
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def decrease_note_count_if_public(actor, object) do
|
||||
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
|
||||
end
|
||||
|
||||
def increase_replies_count_if_reply(%{
|
||||
"object" => %{"inReplyTo" => reply_ap_id} = object,
|
||||
"type" => "Create"
|
||||
}) do
|
||||
if is_public?(object) do
|
||||
Activity.increase_replies_count(reply_ap_id)
|
||||
Object.increase_replies_count(reply_ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
def increase_replies_count_if_reply(_create_data), do: :noop
|
||||
|
||||
def decrease_replies_count_if_reply(%Object{
|
||||
data: %{"inReplyTo" => reply_ap_id} = object
|
||||
}) do
|
||||
if is_public?(object) do
|
||||
Activity.decrease_replies_count(reply_ap_id)
|
||||
Object.decrease_replies_count(reply_ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
def decrease_replies_count_if_reply(_object), do: :noop
|
||||
|
||||
def insert(map, local \\ true, fake \\ 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} <- {:remote_limit_error, check_remote_limit(map)},
|
||||
{:ok, map} <- MRF.filter(map),
|
||||
{recipients, _, _} = get_recipients(map),
|
||||
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
|
||||
{:ok, map, object} <- insert_full_object(map) do
|
||||
{:ok, activity} =
|
||||
Repo.insert(%Activity{
|
||||
data: map,
|
||||
|
|
@ -69,12 +130,39 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
recipients: recipients
|
||||
})
|
||||
|
||||
# Splice in the child object if we have one.
|
||||
activity =
|
||||
if !is_nil(object) do
|
||||
Map.put(activity, :object, object)
|
||||
else
|
||||
activity
|
||||
end
|
||||
|
||||
Task.start(fn ->
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
end)
|
||||
|
||||
Notification.create_notifications(activity)
|
||||
stream_out(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
error -> {:error, error}
|
||||
%Activity{} = activity ->
|
||||
{:ok, activity}
|
||||
|
||||
{:fake, true, map, recipients} ->
|
||||
activity = %Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: recipients,
|
||||
id: "pleroma:fakeid"
|
||||
}
|
||||
|
||||
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
|
||||
{:ok, activity}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -83,7 +171,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
if activity.data["type"] in ["Create", "Announce"] do
|
||||
object = Object.normalize(activity.data["object"])
|
||||
|
||||
Pleroma.Web.Streamer.stream("user", activity)
|
||||
Pleroma.Web.Streamer.stream("list", activity)
|
||||
|
||||
|
|
@ -94,16 +181,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
Pleroma.Web.Streamer.stream("public:local", activity)
|
||||
end
|
||||
|
||||
object.data
|
||||
|> Map.get("tag", [])
|
||||
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|
||||
|> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
|
||||
if activity.data["type"] in ["Create"] do
|
||||
object.data
|
||||
|> Map.get("tag", [])
|
||||
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|
||||
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
|
||||
|
||||
if object.data["attachment"] != [] do
|
||||
Pleroma.Web.Streamer.stream("public:media", activity)
|
||||
if object.data["attachment"] != [] do
|
||||
Pleroma.Web.Streamer.stream("public:media", activity)
|
||||
|
||||
if activity.local do
|
||||
Pleroma.Web.Streamer.stream("public:local:media", activity)
|
||||
if activity.local do
|
||||
Pleroma.Web.Streamer.stream("public:local:media", activity)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
|
|
@ -117,7 +206,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def create(%{to: to, actor: actor, context: context, object: object} = params) do
|
||||
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
|
||||
additional = params[:additional] || %{}
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
|
|
@ -128,10 +217,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
%{to: to, actor: actor, published: published, context: context, object: object},
|
||||
additional
|
||||
),
|
||||
{:ok, activity} <- insert(create_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
{:ok, _actor} <- User.increase_note_count(actor) do
|
||||
{:ok, activity} <- insert(create_data, local, fake),
|
||||
{:fake, false, activity} <- {:fake, fake, activity},
|
||||
_ <- increase_replies_count_if_reply(create_data),
|
||||
# Changing note count prior to enqueuing federation task in order to avoid
|
||||
# race conditions on updating user.info
|
||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:fake, true, activity} ->
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -139,7 +235,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
|
||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
|
||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
|
@ -150,7 +246,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
|
||||
with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
|
||||
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
|
@ -215,10 +311,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
%User{ap_id: _} = user,
|
||||
%Object{data: %{"id" => _}} = object,
|
||||
activity_id \\ nil,
|
||||
local \\ true
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with true <- is_public?(object),
|
||||
announce_data <- make_announce_data(user, object, activity_id),
|
||||
announce_data <- make_announce_data(user, object, activity_id, public),
|
||||
{:ok, activity} <- insert(announce_data, local),
|
||||
{:ok, object} <- add_announce_to_object(activity, object),
|
||||
:ok <- maybe_federate(activity) do
|
||||
|
|
@ -266,18 +363,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with {:ok, _} <- Object.delete(object),
|
||||
with {:ok, object, activity} <- Object.delete(object),
|
||||
data <- %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
{:ok, _actor} <- User.decrease_note_count(user) do
|
||||
_ <- decrease_replies_count_if_reply(object),
|
||||
# Changing note count prior to enqueuing federation task in order to avoid
|
||||
# race conditions on updating user.info
|
||||
{:ok, _actor} <- decrease_note_count_if_public(user, object),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
|
@ -314,6 +415,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def flag(
|
||||
%{
|
||||
actor: actor,
|
||||
context: context,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content
|
||||
} = params
|
||||
) do
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
forward = !(params[:forward] == false)
|
||||
|
||||
additional = params[:additional] || %{}
|
||||
|
||||
params = %{
|
||||
actor: actor,
|
||||
context: context,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content
|
||||
}
|
||||
|
||||
additional =
|
||||
if forward do
|
||||
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
|
||||
else
|
||||
Map.merge(additional, %{"to" => [], "cc" => []})
|
||||
end
|
||||
|
||||
with flag_data <- make_flag_data(params, additional),
|
||||
{:ok, activity} <- insert(flag_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(User.all_superusers(), fn superuser ->
|
||||
superuser
|
||||
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
end)
|
||||
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activities_for_context(context, opts \\ %{}) do
|
||||
public = ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
|
||||
|
|
@ -340,6 +484,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
),
|
||||
order_by: [desc: :id]
|
||||
)
|
||||
|> Activity.with_preloaded_object()
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
|
@ -349,27 +494,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
q
|
||||
|> restrict_unlisted()
|
||||
|> Repo.all()
|
||||
|> Pagination.fetch_paginated(opts)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp restrict_visibility(query, %{visibility: "direct"}) do
|
||||
public = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
join: sender in User,
|
||||
on: sender.ap_id == activity.actor,
|
||||
# Are non-direct statuses with no to/cc possible?
|
||||
where:
|
||||
fragment(
|
||||
"not (? && ?)",
|
||||
[^public, sender.follower_address],
|
||||
activity.recipients
|
||||
defp restrict_visibility(query, %{visibility: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
query =
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
|
||||
|
||||
query
|
||||
else
|
||||
Logger.error("Could not restrict visibility to #{visibility}")
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_visibility(query, %{visibility: visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
query =
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
|
||||
)
|
||||
|
||||
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
|
||||
|
||||
query
|
||||
end
|
||||
|
||||
defp restrict_visibility(_query, %{visibility: visibility})
|
||||
|
|
@ -385,6 +551,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("actor_id", user.ap_id)
|
||||
|> Map.put("whole_db", true)
|
||||
|> Map.put("pinned_activity_ids", user.info.pinned_activities)
|
||||
|
||||
recipients =
|
||||
if reading_user do
|
||||
|
|
@ -398,16 +565,45 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
defp restrict_since(query, %{"since_id" => ""}), do: query
|
||||
|
||||
defp restrict_since(query, %{"since_id" => since_id}) do
|
||||
from(activity in query, where: activity.id > ^since_id)
|
||||
end
|
||||
|
||||
defp restrict_since(query, _), do: query
|
||||
|
||||
defp restrict_tag(query, %{"tag" => tag}) do
|
||||
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
|
||||
when is_list(tag_reject) and tag_reject != [] do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
|
||||
where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_tag_reject(query, _), do: query
|
||||
|
||||
defp restrict_tag_all(query, %{"tag_all" => tag_all})
|
||||
when is_list(tag_all) and tag_all != [] do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_tag_all(query, _), do: query
|
||||
|
||||
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -441,24 +637,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
)
|
||||
end
|
||||
|
||||
defp restrict_limit(query, %{"limit" => limit}) do
|
||||
from(activity in query, limit: ^limit)
|
||||
end
|
||||
|
||||
defp restrict_limit(query, _), do: query
|
||||
|
||||
defp restrict_local(query, %{"local_only" => true}) do
|
||||
from(activity in query, where: activity.local == true)
|
||||
end
|
||||
|
||||
defp restrict_local(query, _), do: query
|
||||
|
||||
defp restrict_max(query, %{"max_id" => max_id}) do
|
||||
from(activity in query, where: activity.id < ^max_id)
|
||||
end
|
||||
|
||||
defp restrict_max(query, _), do: query
|
||||
|
||||
defp restrict_actor(query, %{"actor_id" => actor_id}) do
|
||||
from(activity in query, where: activity.actor == ^actor_id)
|
||||
end
|
||||
|
|
@ -466,7 +650,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp restrict_actor(query, _), do: query
|
||||
|
||||
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
|
||||
restrict_type(query, %{"type" => [type]})
|
||||
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
|
||||
end
|
||||
|
||||
defp restrict_type(query, %{"type" => type}) do
|
||||
|
|
@ -478,7 +662,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data)
|
||||
where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -487,7 +671,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
|
||||
where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -502,15 +686,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_replies(query, _), do: query
|
||||
|
||||
# Only search through last 100_000 activities by default
|
||||
defp restrict_recent(query, %{"whole_db" => true}), do: query
|
||||
|
||||
defp restrict_recent(query, _) do
|
||||
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
|
||||
|
||||
from(activity in query, where: activity.id > ^since)
|
||||
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
|
||||
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
|
||||
end
|
||||
|
||||
defp restrict_reblogs(query, _), do: query
|
||||
|
||||
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
|
||||
mutes = info.mutes
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
|
||||
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_muted(query, _), do: query
|
||||
|
||||
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
|
||||
blocks = info.blocks || []
|
||||
domain_blocks = info.domain_blocks || []
|
||||
|
|
@ -537,47 +732,83 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
)
|
||||
end
|
||||
|
||||
defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
|
||||
from(activity in query, where: activity.id in ^ids)
|
||||
end
|
||||
|
||||
defp restrict_pinned(query, _), do: query
|
||||
|
||||
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
|
||||
muted_reblogs = info.muted_reblogs || []
|
||||
|
||||
from(
|
||||
activity in query,
|
||||
where:
|
||||
fragment(
|
||||
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
|
||||
activity.data,
|
||||
activity.actor,
|
||||
^muted_reblogs
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_muted_reblogs(query, _), do: query
|
||||
|
||||
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
|
||||
|
||||
defp maybe_preload_objects(query, _) do
|
||||
query
|
||||
|> Activity.with_preloaded_object()
|
||||
end
|
||||
|
||||
def fetch_activities_query(recipients, opts \\ %{}) do
|
||||
base_query =
|
||||
from(
|
||||
activity in Activity,
|
||||
limit: 20,
|
||||
order_by: [fragment("? desc nulls last", activity.id)]
|
||||
)
|
||||
base_query = from(activity in Activity)
|
||||
|
||||
base_query
|
||||
|> maybe_preload_objects(opts)
|
||||
|> restrict_recipients(recipients, opts["user"])
|
||||
|> restrict_tag(opts)
|
||||
|> restrict_tag_reject(opts)
|
||||
|> restrict_tag_all(opts)
|
||||
|> restrict_since(opts)
|
||||
|> restrict_local(opts)
|
||||
|> restrict_limit(opts)
|
||||
|> restrict_max(opts)
|
||||
|> restrict_actor(opts)
|
||||
|> restrict_type(opts)
|
||||
|> restrict_favorited_by(opts)
|
||||
|> restrict_recent(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_muted(opts)
|
||||
|> restrict_media(opts)
|
||||
|> restrict_visibility(opts)
|
||||
|> restrict_replies(opts)
|
||||
|> restrict_reblogs(opts)
|
||||
|> restrict_pinned(opts)
|
||||
|> restrict_muted_reblogs(opts)
|
||||
end
|
||||
|
||||
def fetch_activities(recipients, opts \\ %{}) do
|
||||
fetch_activities_query(recipients, opts)
|
||||
|> Repo.all()
|
||||
|> Pagination.fetch_paginated(opts)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
|
||||
fetch_activities_query([], opts)
|
||||
|> restrict_to_cc(recipients_to, recipients_cc)
|
||||
|> Repo.all()
|
||||
|> Pagination.fetch_paginated(opts)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def upload(file, opts \\ []) do
|
||||
with {:ok, data} <- Upload.store(file, opts) do
|
||||
Repo.insert(%Object{data: data})
|
||||
obj_data =
|
||||
if opts[:actor] do
|
||||
Map.put(data, "actor", opts[:actor])
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
Repo.insert(%Object{data: obj_data})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -666,7 +897,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
|
||||
def publish(actor, activity) do
|
||||
followers =
|
||||
remote_followers =
|
||||
if actor.follower_address in activity.recipients do
|
||||
{:ok, followers} = User.get_followers(actor)
|
||||
followers |> Enum.filter(&(!&1.local))
|
||||
|
|
@ -676,84 +907,66 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
public = is_public?(activity)
|
||||
|
||||
remote_inboxes =
|
||||
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %{info: %{source_data: data}} ->
|
||||
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %{info: %{source_data: data}} ->
|
||||
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
|> Enum.each(fn {inbox, unreachable_since} ->
|
||||
Federator.publish_single_ap(%{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
id: activity.data["id"],
|
||||
unreachable_since: unreachable_since
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
|
||||
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
|
||||
Logger.info("Federating #{id} to #{inbox}")
|
||||
host = URI.parse(inbox).host
|
||||
|
||||
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
|
||||
|
||||
date =
|
||||
NaiveDateTime.utc_now()
|
||||
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
|
||||
signature =
|
||||
Pleroma.Web.HTTPSignatures.sign(actor, %{
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
@httpoison.post(
|
||||
inbox,
|
||||
json,
|
||||
[
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest}
|
||||
],
|
||||
hackney: [pool: :default]
|
||||
)
|
||||
end
|
||||
with {:ok, %{status: code}} when code in 200..299 <-
|
||||
result =
|
||||
@httpoison.post(
|
||||
inbox,
|
||||
json,
|
||||
[
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"Date", date},
|
||||
{"signature", signature},
|
||||
{"digest", digest}
|
||||
]
|
||||
) do
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(inbox)
|
||||
|
||||
def is_public?(activity) do
|
||||
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
|
||||
(activity.data["cc"] || []))
|
||||
end
|
||||
|
||||
def visible_for_user?(activity, nil) do
|
||||
is_public?(activity)
|
||||
end
|
||||
|
||||
def visible_for_user?(activity, user) do
|
||||
x = [user.ap_id | user.following]
|
||||
y = activity.data["to"] ++ (activity.data["cc"] || [])
|
||||
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
|
||||
end
|
||||
|
||||
# guard
|
||||
def entire_thread_visible_for_user?(nil, user), do: false
|
||||
|
||||
# child / root
|
||||
def entire_thread_visible_for_user?(
|
||||
%Activity{data: %{"object" => object_id}} = tail,
|
||||
user
|
||||
) do
|
||||
parent = Activity.get_in_reply_to_activity(tail)
|
||||
|
||||
cond do
|
||||
!is_nil(parent) ->
|
||||
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
|
||||
|
||||
true ->
|
||||
visible_for_user?(tail, user)
|
||||
result
|
||||
else
|
||||
{_post_result, response} ->
|
||||
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
|
||||
{:error, response}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
# 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.ActivityPubController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{User, Object}
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
require Logger
|
||||
|
|
@ -13,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
action_fallback(:errors)
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
|
||||
plug(:set_requester_reachable when action in [:inbox])
|
||||
plug(:relay_active? when action in [:relay])
|
||||
|
||||
def relay_active?(conn, _) do
|
||||
|
|
@ -40,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
def object(conn, %{"uuid" => uuid}) do
|
||||
with ap_id <- o_status_url(conn, :object, uuid),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
{_, true} <- {:public?, ActivityPub.is_public?(object)} do
|
||||
{_, true} <- {:public?, Visibility.is_public?(object)} do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ObjectView.render("object.json", %{object: object}))
|
||||
|
|
@ -50,6 +61,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
|
||||
with ap_id <- o_status_url(conn, :object, uuid),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
{_, true} <- {:public?, Visibility.is_public?(object)},
|
||||
likes <- Utils.get_object_likes(object) do
|
||||
{page, _} = Integer.parse(page)
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ObjectView.render("likes.json", ap_id, likes, page))
|
||||
else
|
||||
{:public?, false} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def object_likes(conn, %{"uuid" => uuid}) do
|
||||
with ap_id <- o_status_url(conn, :object, uuid),
|
||||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
||||
{_, true} <- {:public?, Visibility.is_public?(object)},
|
||||
likes <- Utils.get_object_likes(object) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ObjectView.render("likes.json", ap_id, likes))
|
||||
else
|
||||
{:public?, false} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def activity(conn, %{"uuid" => uuid}) do
|
||||
with ap_id <- o_status_url(conn, :activity, uuid),
|
||||
%Activity{} = activity <- Activity.normalize(ap_id),
|
||||
{_, true} <- {:public?, Visibility.is_public?(activity)} do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ObjectView.render("object.json", %{object: activity}))
|
||||
else
|
||||
{:public?, false} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def following(conn, %{"nickname" => nickname, "page" => page}) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
|
||||
|
|
@ -90,30 +144,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def outbox(conn, %{"nickname" => nickname, "max_id" => max_id}) do
|
||||
def outbox(conn, %{"nickname" => nickname} = params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(UserView.render("outbox.json", %{user: user, max_id: max_id}))
|
||||
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
|
||||
end
|
||||
end
|
||||
|
||||
def outbox(conn, %{"nickname" => nickname}) do
|
||||
outbox(conn, %{"nickname" => nickname, "max_id" => nil})
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
true <- Utils.recipient_in_message(user.ap_id, params),
|
||||
params <- Utils.maybe_splice_recipient(user.ap_id, params) do
|
||||
Federator.enqueue(:incoming_ap_doc, params)
|
||||
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
|
||||
%User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]),
|
||||
true <- Utils.recipient_in_message(recipient, actor, params),
|
||||
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
end
|
||||
|
||||
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
|
||||
Federator.enqueue(:incoming_ap_doc, params)
|
||||
Federator.incoming_ap_doc(params)
|
||||
json(conn, "ok")
|
||||
end
|
||||
|
||||
|
|
@ -142,7 +193,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
json(conn, "error")
|
||||
end
|
||||
|
||||
def relay(conn, params) do
|
||||
def relay(conn, _params) do
|
||||
with %User{} = user <- Relay.get_actor(),
|
||||
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
|
||||
conn
|
||||
|
|
@ -153,6 +204,96 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
end
|
||||
end
|
||||
|
||||
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(UserView.render("user.json", %{user: user}))
|
||||
end
|
||||
|
||||
def whoami(_conn, _params), do: {:error, :not_found}
|
||||
|
||||
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
|
||||
if nickname == user.nickname do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
|
||||
else
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json("can't read inbox of #{nickname} as #{user.nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def handle_user_activity(user, %{"type" => "Create"} = params) do
|
||||
object =
|
||||
params["object"]
|
||||
|> Map.merge(Map.take(params, ["to", "cc"]))
|
||||
|> Map.put("attributedTo", user.ap_id())
|
||||
|> Transmogrifier.fix_object()
|
||||
|
||||
ActivityPub.create(%{
|
||||
to: params["to"],
|
||||
actor: user,
|
||||
context: object["context"],
|
||||
object: object,
|
||||
additional: Map.take(params, ["cc"])
|
||||
})
|
||||
end
|
||||
|
||||
def handle_user_activity(user, %{"type" => "Delete"} = params) do
|
||||
with %Object{} = object <- Object.normalize(params["object"]),
|
||||
true <- user.info.is_moderator || user.ap_id == object.data["actor"],
|
||||
{:ok, delete} <- ActivityPub.delete(object) do
|
||||
{:ok, delete}
|
||||
else
|
||||
_ -> {:error, "Can't delete object"}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_user_activity(user, %{"type" => "Like"} = params) do
|
||||
with %Object{} = object <- Object.normalize(params["object"]),
|
||||
{:ok, activity, _object} <- ActivityPub.like(user, object) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, "Can't like object"}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_user_activity(_, _) do
|
||||
{:error, "Unhandled activity type"}
|
||||
end
|
||||
|
||||
def update_outbox(
|
||||
%{assigns: %{user: user}} = conn,
|
||||
%{"nickname" => nickname} = params
|
||||
) do
|
||||
if nickname == user.nickname do
|
||||
actor = user.ap_id()
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.drop(["id"])
|
||||
|> Map.put("actor", actor)
|
||||
|> Transmogrifier.fix_addressing()
|
||||
|
||||
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> put_resp_header("location", activity.data["id"])
|
||||
|> json(activity.data)
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(message)
|
||||
end
|
||||
else
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json("can't update outbox of #{nickname} as #{user.nickname}")
|
||||
end
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(404)
|
||||
|
|
@ -164,4 +305,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
|> put_status(500)
|
||||
|> json("error")
|
||||
end
|
||||
|
||||
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
|
||||
with actor <- conn.params["actor"],
|
||||
true <- is_binary(actor) do
|
||||
Pleroma.Instances.set_reachable(actor)
|
||||
end
|
||||
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# 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 do
|
||||
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
|
||||
|
||||
|
|
@ -12,7 +16,7 @@ defmodule Pleroma.Web.ActivityPub.MRF do
|
|||
end)
|
||||
end
|
||||
|
||||
def get_policies() do
|
||||
def get_policies do
|
||||
Application.get_env(:pleroma, :instance, [])
|
||||
|> Keyword.get(:rewrite_policy, [])
|
||||
|> get_policies()
|
||||
|
|
|
|||
63
lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
Normal file
63
lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
# XXX: this should become User.normalize_by_ap_id() or similar, really.
|
||||
defp normalize_by_ap_id(%{"id" => id}), do: User.get_cached_by_ap_id(id)
|
||||
defp normalize_by_ap_id(uri) when is_binary(uri), do: User.get_cached_by_ap_id(uri)
|
||||
defp normalize_by_ap_id(_), do: nil
|
||||
|
||||
defp score_nickname("followbot@" <> _), do: 1.0
|
||||
defp score_nickname("federationbot@" <> _), do: 1.0
|
||||
defp score_nickname("federation_bot@" <> _), do: 1.0
|
||||
defp score_nickname(_), do: 0.0
|
||||
|
||||
defp score_displayname("federation bot"), do: 1.0
|
||||
defp score_displayname("federationbot"), do: 1.0
|
||||
defp score_displayname("fedibot"), do: 1.0
|
||||
defp score_displayname(_), do: 0.0
|
||||
|
||||
defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
|
||||
# nickname will always be a binary string because it's generated by Pleroma.
|
||||
nick_score =
|
||||
nickname
|
||||
|> String.downcase()
|
||||
|> score_nickname()
|
||||
|
||||
# displayname will either be a binary string or nil, if a displayname isn't set.
|
||||
name_score =
|
||||
if is_binary(displayname) do
|
||||
displayname
|
||||
|> String.downcase()
|
||||
|> score_displayname()
|
||||
else
|
||||
0.0
|
||||
end
|
||||
|
||||
nick_score + name_score
|
||||
end
|
||||
|
||||
defp determine_if_followbot(_), do: 0.0
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
|
||||
%User{} = actor = normalize_by_ap_id(actor_id)
|
||||
|
||||
score = determine_if_followbot(actor)
|
||||
|
||||
# TODO: scan biography data for keywords and score it somehow.
|
||||
if score < 0.8 do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
end
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.DropPolicy do
|
||||
require Logger
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
|
|
|||
44
lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
Normal file
44
lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# 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.EnsureRePrepended do
|
||||
alias Pleroma.Object
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
@reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
|
||||
def filter_by_summary(
|
||||
%{"summary" => parent_summary} = _parent,
|
||||
%{"summary" => child_summary} = child
|
||||
)
|
||||
when not is_nil(child_summary) and byte_size(child_summary) > 0 and
|
||||
not is_nil(parent_summary) and byte_size(parent_summary) > 0 do
|
||||
if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
|
||||
(Regex.match?(@reply_prefix, parent_summary) &&
|
||||
Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
|
||||
Map.put(child, "summary", "re: " <> child_summary)
|
||||
else
|
||||
child
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_summary(_parent, child), do: child
|
||||
|
||||
def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
|
||||
child = object["object"]
|
||||
in_reply_to = Object.normalize(child["inReplyTo"])
|
||||
|
||||
child =
|
||||
if(in_reply_to,
|
||||
do: filter_by_summary(in_reply_to.data, child),
|
||||
else: child
|
||||
)
|
||||
|
||||
object = Map.put(object, "object", child)
|
||||
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
def filter(object), do: {:ok, object}
|
||||
end
|
||||
88
lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
Normal file
88
lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# 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.HellthreadPolicy do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp delist_message(message, threshold) when threshold > 0 do
|
||||
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
|
||||
|
||||
follower_collection? = Enum.member?(message["to"] ++ message["cc"], follower_collection)
|
||||
|
||||
message =
|
||||
case get_recipient_count(message) do
|
||||
{:public, recipients}
|
||||
when follower_collection? and recipients > threshold ->
|
||||
message
|
||||
|> Map.put("to", [follower_collection])
|
||||
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
|
||||
|
||||
{:public, recipients} when recipients > threshold ->
|
||||
message
|
||||
|> Map.put("to", [])
|
||||
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
|
||||
|
||||
_ ->
|
||||
message
|
||||
end
|
||||
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
defp delist_message(message, _threshold), do: {:ok, message}
|
||||
|
||||
defp reject_message(message, threshold) when threshold > 0 do
|
||||
with {_, recipients} <- get_recipient_count(message) do
|
||||
if recipients > threshold do
|
||||
{:reject, nil}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp reject_message(message, _threshold), do: {:ok, message}
|
||||
|
||||
defp get_recipient_count(message) 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
|
||||
recipients =
|
||||
recipients
|
||||
|> List.delete("https://www.w3.org/ns/activitystreams#Public")
|
||||
|> List.delete(follower_collection)
|
||||
|
||||
{:public, length(recipients)}
|
||||
else
|
||||
recipients =
|
||||
recipients
|
||||
|> List.delete(follower_collection)
|
||||
|
||||
{:not_public, length(recipients)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create"} = message) do
|
||||
reject_threshold =
|
||||
Pleroma.Config.get(
|
||||
[:mrf_hellthread, :reject_threshold],
|
||||
Pleroma.Config.get([:mrf_hellthread, :threshold])
|
||||
)
|
||||
|
||||
delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
|
||||
|
||||
with {:ok, message} <- reject_message(message, reject_threshold),
|
||||
{:ok, message} <- delist_message(message, delist_threshold) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e -> {:reject, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
end
|
||||
95
lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
Normal file
95
lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# 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.KeywordPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
defp string_matches?(string, _) when not is_binary(string) do
|
||||
false
|
||||
end
|
||||
|
||||
defp string_matches?(string, pattern) when is_binary(pattern) do
|
||||
String.contains?(string, pattern)
|
||||
end
|
||||
|
||||
defp string_matches?(string, pattern) do
|
||||
String.match?(string, pattern)
|
||||
end
|
||||
|
||||
defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
||||
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
|
||||
string_matches?(content, pattern) or string_matches?(summary, pattern)
|
||||
end) do
|
||||
{:reject, nil}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_ftl_removal(
|
||||
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
|
||||
) do
|
||||
if "https://www.w3.org/ns/activitystreams#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"] || []]
|
||||
|
||||
message =
|
||||
message
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
{:ok, message}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
|
||||
content =
|
||||
if is_binary(content) do
|
||||
content
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
summary =
|
||||
if is_binary(summary) do
|
||||
summary
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
{content, summary} =
|
||||
Enum.reduce(
|
||||
Pleroma.Config.get([:mrf_keyword, :replace]),
|
||||
{content, summary},
|
||||
fn {pattern, replacement}, {content_acc, summary_acc} ->
|
||||
{String.replace(content_acc, pattern, replacement),
|
||||
String.replace(summary_acc, pattern, replacement)}
|
||||
end
|
||||
)
|
||||
|
||||
{:ok,
|
||||
message
|
||||
|> put_in(["object", "content"], content)
|
||||
|> put_in(["object", "summary"], summary)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message) do
|
||||
with {:ok, message} <- check_reject(message),
|
||||
{:ok, message} <- check_ftl_removal(message),
|
||||
{:ok, message} <- check_replace(message) do
|
||||
{:ok, message}
|
||||
else
|
||||
_e ->
|
||||
{:reject, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
@impl true
|
||||
def filter(
|
||||
%{
|
||||
"type" => "Create",
|
||||
"object" => %{"content" => content, "attachment" => _attachment} = child_object
|
||||
} = object
|
||||
)
|
||||
when content in [".", "<p>.</p>"] do
|
||||
child_object =
|
||||
child_object
|
||||
|> Map.put("content", "")
|
||||
|
||||
object =
|
||||
object
|
||||
|> Map.put("object", child_object)
|
||||
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
end
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.NoOpPolicy do
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.NormalizeMarkup do
|
||||
alias Pleroma.HTML
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.RejectNonPublic do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.SimplePolicy do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
|
@ -23,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|
|||
|
||||
defp check_media_removal(
|
||||
%{host: actor_host} = _actor_info,
|
||||
%{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object
|
||||
%{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
object =
|
||||
|
|
|
|||
139
lib/pleroma/web/activity_pub/mrf/tag_policy.ex
Normal file
139
lib/pleroma/web/activity_pub/mrf/tag_policy.ex
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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.TagPolicy do
|
||||
alias Pleroma.User
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
|
||||
defp get_tags(_), do: []
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:media-force-nsfw",
|
||||
%{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
tags = (object["tag"] || []) ++ ["nsfw"]
|
||||
|
||||
object =
|
||||
object
|
||||
|> Map.put("tags", tags)
|
||||
|> Map.put("sensitive", true)
|
||||
|
||||
message = Map.put(message, "object", object)
|
||||
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:media-strip",
|
||||
%{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message
|
||||
)
|
||||
when length(child_attachment) > 0 do
|
||||
object = Map.delete(object, "attachment")
|
||||
message = Map.put(message, "object", object)
|
||||
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:force-unlisted",
|
||||
%{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do
|
||||
to =
|
||||
List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
|
||||
|
||||
cc =
|
||||
List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
|
||||
object =
|
||||
message["object"]
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
message =
|
||||
message
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Map.put("object", object)
|
||||
|
||||
{:ok, message}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:sandbox",
|
||||
%{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or
|
||||
Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do
|
||||
to =
|
||||
List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address]
|
||||
|
||||
cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public")
|
||||
|
||||
object =
|
||||
message["object"]
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
||||
message =
|
||||
message
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|> Map.put("object", object)
|
||||
|
||||
{:ok, message}
|
||||
else
|
||||
{:ok, message}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag(
|
||||
"mrf_tag:disable-remote-subscription",
|
||||
%{"type" => "Follow", "actor" => actor} = message
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
|
||||
if user.local == true do
|
||||
{:ok, message}
|
||||
else
|
||||
{:reject, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil}
|
||||
|
||||
defp process_tag(_, message), do: {:ok, message}
|
||||
|
||||
def filter_message(actor, message) do
|
||||
User.get_cached_by_ap_id(actor)
|
||||
|> get_tags()
|
||||
|> Enum.reduce({:ok, message}, fn
|
||||
tag, {:ok, message} ->
|
||||
process_tag(tag, message)
|
||||
|
||||
_, error ->
|
||||
error
|
||||
end)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"object" => target_actor, "type" => "Follow"} = message),
|
||||
do: filter_message(target_actor, message)
|
||||
|
||||
@impl true
|
||||
def filter(%{"actor" => actor, "type" => "Create"} = message),
|
||||
do: filter_message(actor, message)
|
||||
|
||||
@impl true
|
||||
def filter(message), do: {:ok, message}
|
||||
end
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# 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.UserAllowListPolicy do
|
||||
alias Pleroma.Config
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# 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.Relay do
|
||||
alias Pleroma.{User, Object, Activity}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
require Logger
|
||||
|
||||
|
|
@ -35,8 +41,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
|
||||
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||
with %User{} = user <- get_actor(),
|
||||
%Object{} = object <- Object.normalize(activity.data["object"]) do
|
||||
ActivityPub.announce(user, object)
|
||||
%Object{} = object <- Object.normalize(activity) do
|
||||
ActivityPub.announce(user, object, nil, true, false)
|
||||
else
|
||||
e -> Logger.error("error: #{inspect(e)}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
# 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.Transmogrifier do
|
||||
@moduledoc """
|
||||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.{Containment, Fetcher}
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
|
|
@ -20,8 +27,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def fix_object(object) do
|
||||
object
|
||||
|> fix_actor
|
||||
|> fix_attachments
|
||||
|> fix_url
|
||||
|> fix_attachments
|
||||
|> fix_context
|
||||
|> fix_in_reply_to
|
||||
|> fix_emoji
|
||||
|
|
@ -29,23 +36,107 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> fix_content_map
|
||||
|> fix_likes
|
||||
|> fix_addressing
|
||||
|> fix_summary
|
||||
end
|
||||
|
||||
def fix_summary(%{"summary" => nil} = object) do
|
||||
object
|
||||
|> Map.put("summary", "")
|
||||
end
|
||||
|
||||
def fix_summary(%{"summary" => _} = object) do
|
||||
# summary is present, nothing to do
|
||||
object
|
||||
end
|
||||
|
||||
def fix_summary(object) do
|
||||
object
|
||||
|> Map.put("summary", "")
|
||||
end
|
||||
|
||||
def fix_addressing_list(map, field) do
|
||||
if is_binary(map[field]) do
|
||||
map
|
||||
|> Map.put(field, [map[field]])
|
||||
else
|
||||
map
|
||||
cond do
|
||||
is_binary(map[field]) ->
|
||||
Map.put(map, field, [map[field]])
|
||||
|
||||
is_nil(map[field]) ->
|
||||
Map.put(map, field, [])
|
||||
|
||||
true ->
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
def fix_addressing(map) do
|
||||
map
|
||||
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
|
||||
explicit_to =
|
||||
to
|
||||
|> Enum.filter(fn x -> x in explicit_mentions end)
|
||||
|
||||
explicit_cc =
|
||||
to
|
||||
|> Enum.filter(fn x -> x not in explicit_mentions end)
|
||||
|
||||
final_cc =
|
||||
(cc ++ explicit_cc)
|
||||
|> Enum.uniq()
|
||||
|
||||
object
|
||||
|> Map.put("to", explicit_to)
|
||||
|> Map.put("cc", final_cc)
|
||||
end
|
||||
|
||||
def fix_explicit_addressing(object, _explicit_mentions), do: object
|
||||
|
||||
# if directMessage flag is set to true, leave the addressing alone
|
||||
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
|
||||
|
||||
def fix_explicit_addressing(object) do
|
||||
explicit_mentions =
|
||||
object
|
||||
|> Utils.determine_explicit_mentions()
|
||||
|
||||
explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
|
||||
object
|
||||
|> fix_explicit_addressing(explicit_mentions)
|
||||
end
|
||||
|
||||
# if as:Public is addressed, then make sure the followers collection is also addressed
|
||||
# so that the activities will be delivered to local users.
|
||||
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
|
||||
recipients = to ++ cc
|
||||
|
||||
if followers_collection not in recipients do
|
||||
cond do
|
||||
"https://www.w3.org/ns/activitystreams#Public" in cc ->
|
||||
to = to ++ [followers_collection]
|
||||
Map.put(object, "to", to)
|
||||
|
||||
"https://www.w3.org/ns/activitystreams#Public" in to ->
|
||||
cc = cc ++ [followers_collection]
|
||||
Map.put(object, "cc", cc)
|
||||
|
||||
true ->
|
||||
object
|
||||
end
|
||||
else
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def fix_implicit_addressing(object, _), do: object
|
||||
|
||||
def fix_addressing(object) do
|
||||
%User{} = user = User.get_or_fetch_by_ap_id(object["actor"])
|
||||
followers_collection = User.ap_followers(user)
|
||||
|
||||
object
|
||||
|> fix_addressing_list("to")
|
||||
|> fix_addressing_list("cc")
|
||||
|> fix_addressing_list("bto")
|
||||
|> fix_addressing_list("bcc")
|
||||
|> fix_explicit_addressing
|
||||
|> fix_implicit_addressing(followers_collection)
|
||||
end
|
||||
|
||||
def fix_actor(%{"attributedTo" => actor} = object) do
|
||||
|
|
@ -53,11 +144,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
|
||||
end
|
||||
|
||||
def fix_likes(%{"likes" => likes} = object)
|
||||
when is_bitstring(likes) do
|
||||
# Check for standardisation
|
||||
# This is what Peertube does
|
||||
# curl -H 'Accept: application/activity+json' $likes | jq .totalItems
|
||||
# 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)
|
||||
|
|
@ -87,12 +178,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
case get_obj_helper(in_reply_to_id) do
|
||||
{:ok, replied_object} ->
|
||||
with %Activity{} = activity <-
|
||||
Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
|
||||
with %Activity{} = _activity <-
|
||||
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
|
||||
object
|
||||
|> Map.put("inReplyTo", replied_object.data["id"])
|
||||
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|
||||
|> Map.put("inReplyToStatusId", activity.id)
|
||||
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|
||||
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|
||||
else
|
||||
|
|
@ -121,8 +211,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
attachments =
|
||||
attachment
|
||||
|> Enum.map(fn data ->
|
||||
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
|
||||
Map.put(data, "url", url)
|
||||
media_type = data["mediaType"] || data["mimeType"]
|
||||
href = data["url"] || data["href"]
|
||||
|
||||
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
|
||||
|
||||
data
|
||||
|> Map.put("mediaType", media_type)
|
||||
|> Map.put("url", url)
|
||||
end)
|
||||
|
||||
object
|
||||
|
|
@ -141,7 +237,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("url", url["href"])
|
||||
end
|
||||
|
||||
def fix_url(%{"url" => url} = object) when is_list(url) do
|
||||
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
|
||||
link_element =
|
||||
url
|
||||
|> Enum.filter(fn x -> is_map(x) end)
|
||||
|> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
|
||||
|> Enum.at(0)
|
||||
|
||||
object
|
||||
|> Map.put("attachment", [first_element])
|
||||
|> Map.put("url", link_element["href"])
|
||||
end
|
||||
|
||||
def fix_url(%{"type" => object_type, "url" => url} = object)
|
||||
when object_type != "Video" and is_list(url) do
|
||||
first_element = Enum.at(url, 0)
|
||||
|
||||
url_string =
|
||||
|
|
@ -204,6 +315,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> Map.put("tag", combined)
|
||||
end
|
||||
|
||||
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
|
||||
|
||||
def fix_tag(object), do: object
|
||||
|
||||
# content map usually only has one language so this will do for now.
|
||||
|
|
@ -217,6 +330,66 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_content_map(object), do: object
|
||||
|
||||
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
|
||||
with true <- id =~ "follows",
|
||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
|
||||
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp mastodon_follow_hack(_, _), do: {:error, nil}
|
||||
|
||||
defp get_follow_activity(follow_object, followed) do
|
||||
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
# Can't find the activity. This might a Mastodon 2.3 "Accept"
|
||||
{:activity, nil} ->
|
||||
mastodon_follow_hack(follow_object, followed)
|
||||
|
||||
_ ->
|
||||
{:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
|
||||
# with nil ID.
|
||||
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
|
||||
with context <- data["context"] || Utils.generate_context_id(),
|
||||
content <- data["content"] || "",
|
||||
%User{} = actor <- User.get_cached_by_ap_id(actor),
|
||||
|
||||
# Reduce the object list to find the reported user.
|
||||
%User{} = account <-
|
||||
Enum.reduce_while(objects, nil, fn ap_id, _ ->
|
||||
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
|
||||
{:halt, user}
|
||||
else
|
||||
_ -> {:cont, nil}
|
||||
end
|
||||
end),
|
||||
|
||||
# Remove the reported user from the object list.
|
||||
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
|
||||
params = %{
|
||||
actor: actor,
|
||||
context: context,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content,
|
||||
additional: %{
|
||||
"cc" => [account.ap_id]
|
||||
}
|
||||
}
|
||||
|
||||
ActivityPub.flag(params)
|
||||
end
|
||||
end
|
||||
|
||||
# disallow objects with bogus IDs
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
|
@ -234,7 +407,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
Map.put(data, "actor", actor)
|
||||
|> fix_addressing
|
||||
|
||||
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
|
||||
with nil <- Activity.get_create_by_object_ap_id(object["id"]),
|
||||
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
|
||||
object = fix_object(data["object"])
|
||||
|
||||
|
|
@ -248,6 +421,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
additional:
|
||||
Map.take(data, [
|
||||
"cc",
|
||||
"directMessage",
|
||||
"id"
|
||||
])
|
||||
}
|
||||
|
|
@ -268,7 +442,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
if not User.locked?(followed) do
|
||||
ActivityPub.accept(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed.ap_id,
|
||||
actor: followed,
|
||||
object: data,
|
||||
local: true
|
||||
})
|
||||
|
|
@ -282,34 +456,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
end
|
||||
|
||||
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
|
||||
with true <- id =~ "follows",
|
||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
|
||||
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ -> {:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp mastodon_follow_hack(_), do: {:error, nil}
|
||||
|
||||
defp get_follow_activity(follow_object, followed) do
|
||||
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
|
||||
{:ok, activity}
|
||||
else
|
||||
# Can't find the activity. This might a Mastodon 2.3 "Accept"
|
||||
{:activity, nil} ->
|
||||
mastodon_follow_hack(follow_object, followed)
|
||||
|
||||
_ ->
|
||||
{:error, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -320,12 +468,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
ActivityPub.accept(%{
|
||||
to: follow_activity.data["to"],
|
||||
type: "Accept",
|
||||
actor: followed.ap_id,
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
}) do
|
||||
if not User.following?(follower, followed) do
|
||||
{:ok, follower} = User.follow(follower, followed)
|
||||
{:ok, _follower} = User.follow(follower, followed)
|
||||
end
|
||||
|
||||
{:ok, activity}
|
||||
|
|
@ -335,7 +483,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
%User{} = followed <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -343,10 +491,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
|
||||
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
|
||||
{:ok, activity} <-
|
||||
ActivityPub.accept(%{
|
||||
ActivityPub.reject(%{
|
||||
to: follow_activity.data["to"],
|
||||
type: "Accept",
|
||||
actor: followed.ap_id,
|
||||
type: "Reject",
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
}) do
|
||||
|
|
@ -359,7 +507,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||
|
|
@ -372,12 +520,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
|
||||
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:ok, object} <- get_obj_helper(object_id),
|
||||
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
|
||||
public <- Visibility.is_public?(data),
|
||||
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_e -> :error
|
||||
|
|
@ -443,7 +592,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Announce", "object" => object_id},
|
||||
"actor" => actor,
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
|
|
@ -471,7 +620,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.unfollow(follower, followed)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -490,12 +639,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.unblock(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
|
||||
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
|
||||
) do
|
||||
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
|
||||
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
|
||||
|
|
@ -505,7 +654,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
User.block(blocker, blocked)
|
||||
{:ok, activity}
|
||||
else
|
||||
e -> :error
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -513,7 +662,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{
|
||||
"type" => "Undo",
|
||||
"object" => %{"type" => "Like", "object" => object_id},
|
||||
"actor" => actor,
|
||||
"actor" => _actor,
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
|
|
@ -533,10 +682,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
if object = Object.normalize(id), do: {:ok, object}, else: nil
|
||||
end
|
||||
|
||||
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
|
||||
with false <- String.starts_with?(inReplyTo, "http"),
|
||||
{:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
|
||||
Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
|
||||
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
|
||||
with false <- String.starts_with?(in_reply_to, "http"),
|
||||
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
|
||||
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
|
||||
else
|
||||
_e -> object
|
||||
end
|
||||
|
|
@ -552,6 +701,7 @@ 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
|
||||
|
|
@ -618,6 +768,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def prepare_outgoing(%{"type" => _type} = data) do
|
||||
data =
|
||||
data
|
||||
|> strip_internal_fields
|
||||
|> maybe_fix_object_url
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
|
||||
|
|
@ -648,12 +799,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
def add_hashtags(object) do
|
||||
tags =
|
||||
(object["tag"] || [])
|
||||
|> Enum.map(fn tag ->
|
||||
%{
|
||||
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
|
||||
"name" => "##{tag}",
|
||||
"type" => "Hashtag"
|
||||
}
|
||||
|> Enum.map(fn
|
||||
# Expand internal representation tags into AS2 tags.
|
||||
tag when is_binary(tag) ->
|
||||
%{
|
||||
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
|
||||
"name" => "##{tag}",
|
||||
"type" => "Hashtag"
|
||||
}
|
||||
|
||||
# Do not process tags which are already AS2 tag objects.
|
||||
tag when is_map(tag) ->
|
||||
tag
|
||||
end)
|
||||
|
||||
object
|
||||
|
|
@ -705,10 +862,26 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def add_attributed_to(object) do
|
||||
attributedTo = object["attributedTo"] || object["actor"]
|
||||
attributed_to = object["attributedTo"] || object["actor"]
|
||||
|
||||
object
|
||||
|> Map.put("attributedTo", attributedTo)
|
||||
|> 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
|
||||
|
|
@ -726,12 +899,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
defp strip_internal_fields(object) do
|
||||
object
|
||||
|> Map.drop([
|
||||
"likes",
|
||||
"like_count",
|
||||
"announcements",
|
||||
"announcement_count",
|
||||
"emoji",
|
||||
"context_id"
|
||||
"context_id",
|
||||
"deleted_activity_id"
|
||||
])
|
||||
end
|
||||
|
||||
|
|
@ -746,8 +919,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
defp strip_internal_tags(object), do: object
|
||||
|
||||
defp user_upgrade_task(user) do
|
||||
old_follower_address = User.ap_followers(user)
|
||||
def perform(:user_upgrade, user) do
|
||||
# we pass a fake user so that the followers collection is stripped away
|
||||
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
||||
|
||||
q =
|
||||
from(
|
||||
|
|
@ -770,15 +944,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
maybe_retire_websub(user.ap_id)
|
||||
|
||||
# Only do this for recent activties, don't go through the whole db.
|
||||
# Only look at the last 1000 activities.
|
||||
since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
|
||||
|
||||
q =
|
||||
from(
|
||||
a in Activity,
|
||||
where: ^old_follower_address in a.recipients,
|
||||
where: a.id > ^since,
|
||||
update: [
|
||||
set: [
|
||||
recipients:
|
||||
|
|
@ -795,28 +964,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
Repo.update_all(q, [])
|
||||
end
|
||||
|
||||
def upgrade_user_from_ap_id(ap_id, async \\ true) do
|
||||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
|
||||
already_ap = User.ap_enabled?(user)
|
||||
|
||||
{:ok, user} =
|
||||
User.upgrade_changeset(user, data)
|
||||
|> Repo.update()
|
||||
|
||||
if !already_ap do
|
||||
# This could potentially take a long time, do it in the background
|
||||
if async do
|
||||
Task.start(fn ->
|
||||
user_upgrade_task(user)
|
||||
end)
|
||||
else
|
||||
user_upgrade_task(user)
|
||||
end
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
already_ap <- User.ap_enabled?(user),
|
||||
{:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
|
||||
unless already_ap do
|
||||
PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
|
||||
end
|
||||
|
||||
{:ok, user}
|
||||
else
|
||||
%User{} = user -> {:ok, user}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,22 @@
|
|||
# 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.Utils do
|
||||
alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
alias Ecto.Changeset
|
||||
alias Ecto.UUID
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
||||
@supported_object_types ["Article", "Note", "Video", "Page"]
|
||||
|
|
@ -21,11 +34,25 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
Map.put(params, "actor", get_ap_id(params["actor"]))
|
||||
end
|
||||
|
||||
def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do
|
||||
tag
|
||||
|> Enum.filter(fn x -> is_map(x) end)
|
||||
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|
||||
|> Enum.map(fn x -> x["href"] end)
|
||||
end
|
||||
|
||||
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
|
||||
Map.put(object, "tag", [tag])
|
||||
|> determine_explicit_mentions()
|
||||
end
|
||||
|
||||
def determine_explicit_mentions(_), do: []
|
||||
|
||||
defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
|
||||
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
|
||||
defp recipient_in_collection(_, _), do: false
|
||||
|
||||
def recipient_in_message(ap_id, params) do
|
||||
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
|
||||
cond do
|
||||
recipient_in_collection(ap_id, params["to"]) ->
|
||||
true
|
||||
|
|
@ -44,6 +71,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
!params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
|
||||
true
|
||||
|
||||
# if the message is sent from somebody the user is following, then assume it
|
||||
# is addressed to the recipient
|
||||
User.following?(recipient, actor) ->
|
||||
true
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
|
|
@ -72,7 +104,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
%{
|
||||
"@context" => [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"#{Web.base_url()}/schemas/litepub-0.1.jsonld"
|
||||
"#{Web.base_url()}/schemas/litepub-0.1.jsonld",
|
||||
%{
|
||||
"@language" => "und"
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
|
@ -138,7 +173,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
_ -> 5
|
||||
end
|
||||
|
||||
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
|
||||
Pleroma.Web.Federator.publish(activity, priority)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -148,18 +183,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
Adds an id and a published data if they aren't there,
|
||||
also adds it to an included object
|
||||
"""
|
||||
def lazy_put_activity_defaults(map) do
|
||||
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
|
||||
|
||||
def lazy_put_activity_defaults(map, fake \\ false) do
|
||||
map =
|
||||
map
|
||||
|> Map.put_new_lazy("id", &generate_activity_id/0)
|
||||
|> Map.put_new_lazy("published", &make_date/0)
|
||||
|> Map.put_new("context", context)
|
||||
|> Map.put_new("context_id", context_id)
|
||||
unless fake do
|
||||
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
|
||||
|
||||
map
|
||||
|> Map.put_new_lazy("id", &generate_activity_id/0)
|
||||
|> Map.put_new_lazy("published", &make_date/0)
|
||||
|> Map.put_new("context", context)
|
||||
|> Map.put_new("context_id", context_id)
|
||||
else
|
||||
map
|
||||
|> Map.put_new("id", "pleroma:fakeid")
|
||||
|> Map.put_new_lazy("published", &make_date/0)
|
||||
|> Map.put_new("context", "pleroma:fakecontext")
|
||||
|> Map.put_new("context_id", -1)
|
||||
end
|
||||
|
||||
if is_map(map["object"]) do
|
||||
object = lazy_put_object_defaults(map["object"], map)
|
||||
object = lazy_put_object_defaults(map["object"], map, fake)
|
||||
%{map | "object" => object}
|
||||
else
|
||||
map
|
||||
|
|
@ -169,7 +212,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
@doc """
|
||||
Adds an id and published date if they aren't there.
|
||||
"""
|
||||
def lazy_put_object_defaults(map, activity \\ %{}) do
|
||||
def lazy_put_object_defaults(map, activity \\ %{}, fake)
|
||||
|
||||
def lazy_put_object_defaults(map, activity, true = _fake) do
|
||||
map
|
||||
|> Map.put_new_lazy("published", &make_date/0)
|
||||
|> Map.put_new("id", "pleroma:fake_object_id")
|
||||
|> Map.put_new("context", activity["context"])
|
||||
|> Map.put_new("fake", true)
|
||||
|> Map.put_new("context_id", activity["context_id"])
|
||||
end
|
||||
|
||||
def lazy_put_object_defaults(map, activity, _fake) do
|
||||
map
|
||||
|> Map.put_new_lazy("id", &generate_object_id/0)
|
||||
|> Map.put_new_lazy("published", &make_date/0)
|
||||
|
|
@ -187,18 +241,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
map
|
||||
|> Map.put("object", object.data["id"])
|
||||
|
||||
{:ok, map}
|
||||
{:ok, map, object}
|
||||
end
|
||||
end
|
||||
|
||||
def insert_full_object(map), do: {:ok, map}
|
||||
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.all_by_object_ap_id(id)
|
||||
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)
|
||||
|
|
@ -231,13 +285,52 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
Repo.one(query)
|
||||
end
|
||||
|
||||
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
|
||||
@doc """
|
||||
Returns like activities targeting an object
|
||||
"""
|
||||
def get_object_likes(%{data: %{"id" => id}}) do
|
||||
query =
|
||||
from(
|
||||
activity in Activity,
|
||||
# this is to use the index
|
||||
where:
|
||||
fragment(
|
||||
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
|
||||
activity.data,
|
||||
activity.data,
|
||||
^id
|
||||
),
|
||||
where: fragment("(?)->>'type' = 'Like'", activity.data)
|
||||
)
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def make_like_data(
|
||||
%User{ap_id: ap_id} = actor,
|
||||
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
|
||||
activity_id
|
||||
) do
|
||||
object_actor = User.get_cached_by_ap_id(object_actor_id)
|
||||
|
||||
to =
|
||||
if Visibility.is_public?(object) do
|
||||
[actor.follower_address, object.data["actor"]]
|
||||
else
|
||||
[object.data["actor"]]
|
||||
end
|
||||
|
||||
cc =
|
||||
(object.data["to"] ++ (object.data["cc"] || []))
|
||||
|> List.delete(actor.ap_id)
|
||||
|> List.delete(object_actor.follower_address)
|
||||
|
||||
data = %{
|
||||
"type" => "Like",
|
||||
"actor" => ap_id,
|
||||
"object" => id,
|
||||
"to" => [actor.follower_address, object.data["actor"]],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"to" => to,
|
||||
"cc" => cc,
|
||||
"context" => object.data["context"]
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +343,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} <- Repo.update(changeset),
|
||||
{:ok, object} <- Object.update_and_set_cache(changeset),
|
||||
_ <- update_object_in_activities(object) do
|
||||
{:ok, object}
|
||||
end
|
||||
|
|
@ -281,6 +374,25 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
@doc """
|
||||
Updates a follow activity's state (for locked accounts).
|
||||
"""
|
||||
def update_follow_state(
|
||||
%Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
|
||||
state
|
||||
) do
|
||||
try do
|
||||
Ecto.Adapters.SQL.query!(
|
||||
Repo,
|
||||
"UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
|
||||
[state, actor, object]
|
||||
)
|
||||
|
||||
activity = Activity.get_by_id(activity.id)
|
||||
{:ok, activity}
|
||||
rescue
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def update_follow_state(%Activity{} = activity, state) do
|
||||
with new_data <-
|
||||
activity.data
|
||||
|
|
@ -296,7 +408,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"""
|
||||
def make_follow_data(
|
||||
%User{ap_id: follower_id},
|
||||
%User{ap_id: followed_id} = followed,
|
||||
%User{ap_id: followed_id} = _followed,
|
||||
activity_id
|
||||
) do
|
||||
data = %{
|
||||
|
|
@ -323,13 +435,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
activity.data
|
||||
),
|
||||
where: activity.actor == ^follower_id,
|
||||
# this is to use the index
|
||||
where:
|
||||
fragment(
|
||||
"? @> ?",
|
||||
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
|
||||
activity.data,
|
||||
^%{object: followed_id}
|
||||
activity.data,
|
||||
^followed_id
|
||||
),
|
||||
order_by: [desc: :id],
|
||||
order_by: [fragment("? desc nulls last", activity.id)],
|
||||
limit: 1
|
||||
)
|
||||
|
||||
|
|
@ -365,9 +479,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"""
|
||||
# for relayed messages, we only want to send to subscribers
|
||||
def make_announce_data(
|
||||
%User{ap_id: ap_id, nickname: nil} = user,
|
||||
%User{ap_id: ap_id} = user,
|
||||
%Object{data: %{"id" => id}} = object,
|
||||
activity_id
|
||||
activity_id,
|
||||
false
|
||||
) do
|
||||
data = %{
|
||||
"type" => "Announce",
|
||||
|
|
@ -384,7 +499,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
def make_announce_data(
|
||||
%User{ap_id: ap_id} = user,
|
||||
%Object{data: %{"id" => id}} = object,
|
||||
activity_id
|
||||
activity_id,
|
||||
true
|
||||
) do
|
||||
data = %{
|
||||
"type" => "Announce",
|
||||
|
|
@ -484,13 +600,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
activity.data
|
||||
),
|
||||
where: activity.actor == ^blocker_id,
|
||||
# this is to use the index
|
||||
where:
|
||||
fragment(
|
||||
"? @> ?",
|
||||
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
|
||||
activity.data,
|
||||
^%{object: blocked_id}
|
||||
activity.data,
|
||||
^blocked_id
|
||||
),
|
||||
order_by: [desc: :id],
|
||||
order_by: [fragment("? desc nulls last", activity.id)],
|
||||
limit: 1
|
||||
)
|
||||
|
||||
|
|
@ -534,4 +652,65 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
||||
#### Flag-related helpers
|
||||
|
||||
def make_flag_data(params, additional) do
|
||||
status_ap_ids =
|
||||
Enum.map(params.statuses || [], fn
|
||||
%Activity{} = act -> act.data["id"]
|
||||
act when is_map(act) -> act["id"]
|
||||
act when is_binary(act) -> act
|
||||
end)
|
||||
|
||||
object = [params.account.ap_id] ++ status_ap_ids
|
||||
|
||||
%{
|
||||
"type" => "Flag",
|
||||
"actor" => params.actor.ap_id,
|
||||
"content" => params.content,
|
||||
"object" => object,
|
||||
"context" => params.context
|
||||
}
|
||||
|> Map.merge(additional)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
|
||||
the first one to `pages_left` pages.
|
||||
If the amount of pages is higher than the collection has, it returns whatever was there.
|
||||
"""
|
||||
def fetch_ordered_collection(from, pages_left, acc \\ []) do
|
||||
with {:ok, response} <- Tesla.get(from),
|
||||
{:ok, collection} <- Poison.decode(response.body) do
|
||||
case collection["type"] do
|
||||
"OrderedCollection" ->
|
||||
# If we've encountered the OrderedCollection and not the page,
|
||||
# just call the same function on the page address
|
||||
fetch_ordered_collection(collection["first"], pages_left)
|
||||
|
||||
"OrderedCollectionPage" ->
|
||||
if pages_left > 0 do
|
||||
# There are still more pages
|
||||
if Map.has_key?(collection, "next") do
|
||||
# There are still more pages, go deeper saving what we have into the accumulator
|
||||
fetch_ordered_collection(
|
||||
collection["next"],
|
||||
pages_left - 1,
|
||||
acc ++ collection["orderedItems"]
|
||||
)
|
||||
else
|
||||
# No more pages left, just return whatever we already have
|
||||
acc ++ collection["orderedItems"]
|
||||
end
|
||||
else
|
||||
# Got the amount of pages needed, add them all to the accumulator
|
||||
acc ++ collection["orderedItems"]
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, "Not an OrderedCollection or OrderedCollectionPage"}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# 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.ObjectView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.{Object, Activity}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
def render("object.json", %{object: %Object{} = object}) do
|
||||
|
|
@ -12,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
|
||||
def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
|
||||
object = Object.normalize(activity.data["object"])
|
||||
object = Object.normalize(activity)
|
||||
|
||||
additional =
|
||||
Transmogrifier.prepare_object(activity.data)
|
||||
|
|
@ -23,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
|
||||
def render("object.json", %{object: %Activity{} = activity}) do
|
||||
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
|
||||
object = Object.normalize(activity.data["object"])
|
||||
object = Object.normalize(activity)
|
||||
|
||||
additional =
|
||||
Transmogrifier.prepare_object(activity.data)
|
||||
|
|
@ -31,4 +36,38 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
|
|||
|
||||
Map.merge(base, additional)
|
||||
end
|
||||
|
||||
def render("likes.json", ap_id, likes, page) do
|
||||
collection(likes, "#{ap_id}/likes", page)
|
||||
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("likes.json", ap_id, likes) do
|
||||
%{
|
||||
"id" => "#{ap_id}/likes",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => length(likes),
|
||||
"first" => collection(likes, "#{ap_id}/likes", 1)
|
||||
}
|
||||
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def collection(collection, iri, page) do
|
||||
offset = (page - 1) * 10
|
||||
items = Enum.slice(collection, offset, 10)
|
||||
items = Enum.map(items, fn object -> Transmogrifier.prepare_object(object.data) end)
|
||||
total = length(collection)
|
||||
|
||||
map = %{
|
||||
"id" => "#{iri}?page=#{page}",
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => total,
|
||||
"orderedItems" => items
|
||||
}
|
||||
|
||||
if offset < total do
|
||||
Map.put(map, "next", "#{iri}?page=#{page + 1}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +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.UserView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.Salmon
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Pleroma.User
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
alias Pleroma.Web.Salmon
|
||||
alias Pleroma.Web.WebFinger
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
|
||||
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
|
||||
end
|
||||
|
||||
def render("endpoints.json", %{user: %User{local: true} = _user}) do
|
||||
%{
|
||||
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
|
||||
"oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app),
|
||||
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
|
||||
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)
|
||||
}
|
||||
end
|
||||
|
||||
def render("endpoints.json", _), do: %{}
|
||||
|
||||
# the instance itself is not a Person, but instead an Application
|
||||
def render("user.json", %{user: %{nickname: nil} = user}) do
|
||||
{:ok, user} = WebFinger.ensure_keys_present(user)
|
||||
|
|
@ -16,6 +39,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => "Application",
|
||||
|
|
@ -31,9 +56,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
"endpoints" => %{
|
||||
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
|
||||
}
|
||||
"endpoints" => endpoints
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
|
@ -44,6 +67,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
public_key = :public_key.pem_encode([public_key])
|
||||
|
||||
endpoints = render("endpoints.json", %{user: user})
|
||||
|
||||
%{
|
||||
"id" => user.ap_id,
|
||||
"type" => "Person",
|
||||
|
|
@ -61,19 +86,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"owner" => user.ap_id,
|
||||
"publicKeyPem" => public_key
|
||||
},
|
||||
"endpoints" => %{
|
||||
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
|
||||
},
|
||||
"icon" => %{
|
||||
"type" => "Image",
|
||||
"url" => User.avatar_url(user)
|
||||
},
|
||||
"image" => %{
|
||||
"type" => "Image",
|
||||
"url" => User.banner_url(user)
|
||||
},
|
||||
"endpoints" => endpoints,
|
||||
"tag" => user.info.source_data["tag"] || []
|
||||
}
|
||||
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|
||||
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -82,7 +99,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
following = Repo.all(query)
|
||||
|
||||
collection(following, "#{user.ap_id}/following", page)
|
||||
total =
|
||||
if !user.info.hide_follows do
|
||||
length(following)
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -91,11 +115,18 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
following = Repo.all(query)
|
||||
|
||||
total =
|
||||
if !user.info.hide_follows do
|
||||
length(following)
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
%{
|
||||
"id" => "#{user.ap_id}/following",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => length(following),
|
||||
"first" => collection(following, "#{user.ap_id}/following", 1)
|
||||
"totalItems" => total,
|
||||
"first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
|
@ -105,7 +136,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
followers = Repo.all(query)
|
||||
|
||||
collection(followers, "#{user.ap_id}/followers", page)
|
||||
total =
|
||||
if !user.info.hide_followers do
|
||||
length(followers)
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total)
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
|
|
@ -114,19 +152,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
query = from(user in query, select: [:ap_id])
|
||||
followers = Repo.all(query)
|
||||
|
||||
total =
|
||||
if !user.info.hide_followers do
|
||||
length(followers)
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
%{
|
||||
"id" => "#{user.ap_id}/followers",
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => length(followers),
|
||||
"first" => collection(followers, "#{user.ap_id}/followers", 1)
|
||||
"totalItems" => total,
|
||||
"first" =>
|
||||
collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total)
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
|
||||
def render("outbox.json", %{user: user, max_id: max_qid}) do
|
||||
# XXX: technically note_count is wrong for this, but it's better than nothing
|
||||
info = User.user_info(user)
|
||||
|
||||
params = %{
|
||||
"limit" => "10"
|
||||
}
|
||||
|
|
@ -139,6 +182,61 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
|
||||
activities = ActivityPub.fetch_user_activities(user, nil, params)
|
||||
|
||||
{max_id, min_id, collection} =
|
||||
if length(activities) > 0 do
|
||||
{
|
||||
Enum.at(Enum.reverse(activities), 0).id,
|
||||
Enum.at(activities, 0).id,
|
||||
Enum.map(activities, fn act ->
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
|
||||
data
|
||||
end)
|
||||
}
|
||||
else
|
||||
{
|
||||
0,
|
||||
0,
|
||||
[]
|
||||
}
|
||||
end
|
||||
|
||||
iri = "#{user.ap_id}/outbox"
|
||||
|
||||
page = %{
|
||||
"id" => "#{iri}?max_id=#{max_id}",
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"orderedItems" => collection,
|
||||
"next" => "#{iri}?max_id=#{min_id}"
|
||||
}
|
||||
|
||||
if max_qid == nil do
|
||||
%{
|
||||
"id" => iri,
|
||||
"type" => "OrderedCollection",
|
||||
"first" => page
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
else
|
||||
page |> Map.merge(Utils.make_json_ld_header())
|
||||
end
|
||||
end
|
||||
|
||||
def render("inbox.json", %{user: user, max_id: max_qid}) do
|
||||
params = %{
|
||||
"limit" => "10"
|
||||
}
|
||||
|
||||
params =
|
||||
if max_qid != nil do
|
||||
Map.put(params, "max_id", max_qid)
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
|
||||
|
||||
min_id = Enum.at(Enum.reverse(activities), 0).id
|
||||
max_id = Enum.at(activities, 0).id
|
||||
|
||||
|
|
@ -148,22 +246,20 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
data
|
||||
end)
|
||||
|
||||
iri = "#{user.ap_id}/outbox"
|
||||
iri = "#{user.ap_id}/inbox"
|
||||
|
||||
page = %{
|
||||
"id" => "#{iri}?max_id=#{max_id}",
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => info.note_count,
|
||||
"orderedItems" => collection,
|
||||
"next" => "#{iri}?max_id=#{min_id - 1}"
|
||||
"next" => "#{iri}?max_id=#{min_id}"
|
||||
}
|
||||
|
||||
if max_qid == nil do
|
||||
%{
|
||||
"id" => iri,
|
||||
"type" => "OrderedCollection",
|
||||
"totalItems" => info.note_count,
|
||||
"first" => page
|
||||
}
|
||||
|> Map.merge(Utils.make_json_ld_header())
|
||||
|
|
@ -172,7 +268,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
end
|
||||
end
|
||||
|
||||
def collection(collection, iri, page, total \\ nil) do
|
||||
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
|
||||
offset = (page - 1) * 10
|
||||
items = Enum.slice(collection, offset, 10)
|
||||
items = Enum.map(items, fn user -> user.ap_id end)
|
||||
|
|
@ -183,11 +279,26 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|
|||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => iri,
|
||||
"totalItems" => total,
|
||||
"orderedItems" => items
|
||||
"orderedItems" => if(show_items, do: items, else: [])
|
||||
}
|
||||
|
||||
if offset < total do
|
||||
Map.put(map, "next", "#{iri}?page=#{page + 1}")
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_make_image(func, key, user) do
|
||||
if image = func.(user, no_default: true) do
|
||||
%{
|
||||
key => %{
|
||||
"type" => "Image",
|
||||
"url" => image
|
||||
}
|
||||
}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
56
lib/pleroma/web/activity_pub/visibility.ex
Normal file
56
lib/pleroma/web/activity_pub/visibility.ex
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
defmodule Pleroma.Web.ActivityPub.Visibility do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
|
||||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
|
||||
def is_public?(%Object{data: data}), do: is_public?(data)
|
||||
def is_public?(%Activity{data: data}), do: is_public?(data)
|
||||
def is_public?(%{"directMessage" => true}), do: false
|
||||
|
||||
def is_public?(data) do
|
||||
"https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || []))
|
||||
end
|
||||
|
||||
def is_private?(activity) do
|
||||
unless is_public?(activity) do
|
||||
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address
|
||||
Enum.any?(activity.data["to"], &(&1 == follower_address))
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
|
||||
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
|
||||
|
||||
def is_direct?(activity) do
|
||||
!is_public?(activity) && !is_private?(activity)
|
||||
end
|
||||
|
||||
def visible_for_user?(activity, nil) do
|
||||
is_public?(activity)
|
||||
end
|
||||
|
||||
def visible_for_user?(activity, user) do
|
||||
x = [user.ap_id | user.following]
|
||||
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
|
||||
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
|
||||
end
|
||||
|
||||
# guard
|
||||
def entire_thread_visible_for_user?(nil, _user), do: false
|
||||
|
||||
# child
|
||||
def entire_thread_visible_for_user?(
|
||||
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
|
||||
user
|
||||
)
|
||||
when is_binary(parent_id) do
|
||||
parent = Activity.get_in_reply_to_activity(tail)
|
||||
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
|
||||
end
|
||||
|
||||
# root
|
||||
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
|
||||
end
|
||||
|
|
@ -1,30 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserInviteToken
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.AdminAPI.Search
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
|
||||
|
||||
require Logger
|
||||
|
||||
@users_page_size 50
|
||||
|
||||
action_fallback(:errors)
|
||||
|
||||
def user_delete(conn, %{"nickname" => nickname}) do
|
||||
user = User.get_by_nickname(nickname)
|
||||
|
||||
if user.local == true do
|
||||
User.delete(user)
|
||||
else
|
||||
User.delete(user)
|
||||
end
|
||||
User.get_by_nickname(nickname)
|
||||
|> User.delete()
|
||||
|
||||
conn
|
||||
|> json(nickname)
|
||||
end
|
||||
|
||||
def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
|
||||
with %User{} = follower <- User.get_by_nickname(follower_nick),
|
||||
%User{} = followed <- User.get_by_nickname(followed_nick) do
|
||||
User.follow(follower, followed)
|
||||
end
|
||||
|
||||
conn
|
||||
|> json("ok")
|
||||
end
|
||||
|
||||
def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
|
||||
with %User{} = follower <- User.get_by_nickname(follower_nick),
|
||||
%User{} = followed <- User.get_by_nickname(followed_nick) do
|
||||
User.unfollow(follower, followed)
|
||||
end
|
||||
|
||||
conn
|
||||
|> json("ok")
|
||||
end
|
||||
|
||||
def user_create(
|
||||
conn,
|
||||
%{"nickname" => nickname, "email" => email, "password" => password}
|
||||
) do
|
||||
new_user = %{
|
||||
user_data = %{
|
||||
nickname: nickname,
|
||||
name: nickname,
|
||||
email: email,
|
||||
|
|
@ -33,11 +59,74 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
bio: "."
|
||||
}
|
||||
|
||||
User.register_changeset(%User{}, new_user)
|
||||
|> Repo.insert!()
|
||||
changeset = User.register_changeset(%User{}, user_data, confirmed: true)
|
||||
{:ok, user} = User.register(changeset)
|
||||
|
||||
conn
|
||||
|> json(new_user.nickname)
|
||||
|> json(user.nickname)
|
||||
end
|
||||
|
||||
def user_show(conn, %{"nickname" => nickname}) do
|
||||
with %User{} = user <- User.get_by_nickname(nickname) do
|
||||
conn
|
||||
|> json(AccountView.render("show.json", %{user: user}))
|
||||
else
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def user_toggle_activation(conn, %{"nickname" => nickname}) do
|
||||
user = User.get_by_nickname(nickname)
|
||||
|
||||
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
|
||||
|
||||
conn
|
||||
|> json(AccountView.render("show.json", %{user: updated_user}))
|
||||
end
|
||||
|
||||
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.untag(nicknames, tags),
|
||||
do: json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def list_users(conn, params) do
|
||||
{page, page_size} = page_params(params)
|
||||
filters = maybe_parse_filters(params["filters"])
|
||||
|
||||
search_params = %{
|
||||
query: params["query"],
|
||||
page: page,
|
||||
page_size: page_size
|
||||
}
|
||||
|
||||
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
|
||||
do:
|
||||
conn
|
||||
|> json(
|
||||
AccountView.render("index.json",
|
||||
users: users,
|
||||
count: count,
|
||||
page_size: page_size
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@filters ~w(local external active deactivated)
|
||||
|
||||
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
|
||||
|
||||
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
|
||||
defp maybe_parse_filters(filters) do
|
||||
filters
|
||||
|> String.split(",")
|
||||
|> Enum.filter(&Enum.member?(@filters, &1))
|
||||
|> Enum.map(&String.to_atom(&1))
|
||||
|> Enum.into(%{}, &{&1, true})
|
||||
end
|
||||
|
||||
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
|
||||
|
|
@ -51,13 +140,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
info_cng = User.Info.admin_api_update(user.info, info)
|
||||
|
||||
cng =
|
||||
Ecto.Changeset.change(user)
|
||||
user
|
||||
|> Ecto.Changeset.change()
|
||||
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_add(conn, _) do
|
||||
conn
|
||||
|> json(info)
|
||||
|> put_status(404)
|
||||
|> json(%{error: "No such permission_group"})
|
||||
end
|
||||
|
||||
def right_get(conn, %{"nickname" => nickname}) do
|
||||
|
|
@ -70,12 +165,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
})
|
||||
end
|
||||
|
||||
def right_add(conn, _) do
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json(%{error: "No such permission_group"})
|
||||
end
|
||||
|
||||
def right_delete(
|
||||
%{assigns: %{user: %User{:nickname => admin_nickname}}} = conn,
|
||||
%{
|
||||
|
|
@ -101,10 +190,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
Ecto.Changeset.change(user)
|
||||
|> Ecto.Changeset.put_embed(:info, info_cng)
|
||||
|
||||
{:ok, user} = User.update_and_set_cache(cng)
|
||||
{:ok, _user} = User.update_and_set_cache(cng)
|
||||
|
||||
conn
|
||||
|> json(info)
|
||||
json(conn, info)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -114,41 +202,80 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(%{error: "No such permission_group"})
|
||||
end
|
||||
|
||||
def relay_follow(conn, %{"relay_url" => target}) do
|
||||
{status, message} = Relay.follow(target)
|
||||
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
|
||||
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
|
||||
%User{} = user <- User.get_by_nickname(nickname),
|
||||
{:ok, _} <- User.deactivate(user, !status),
|
||||
do: json_response(conn, :no_content, "")
|
||||
end
|
||||
|
||||
if status == :ok do
|
||||
conn
|
||||
|> json(target)
|
||||
def relay_follow(conn, %{"relay_url" => target}) do
|
||||
with {:ok, _message} <- Relay.follow(target) do
|
||||
json(conn, target)
|
||||
else
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
def relay_unfollow(conn, %{"relay_url" => target}) do
|
||||
{status, message} = Relay.unfollow(target)
|
||||
|
||||
if status == :ok do
|
||||
conn
|
||||
|> json(target)
|
||||
with {:ok, _message} <- Relay.unfollow(target) do
|
||||
json(conn, target)
|
||||
else
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json(target)
|
||||
end
|
||||
end
|
||||
|
||||
@shortdoc "Get a account registeration invite token (base64 string)"
|
||||
def get_invite_token(conn, _params) do
|
||||
{:ok, token} = Pleroma.UserInviteToken.create_token()
|
||||
|
||||
conn
|
||||
|> json(token.token)
|
||||
@doc "Sends registration invite via email"
|
||||
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
|
||||
with true <-
|
||||
Pleroma.Config.get([:instance, :invites_enabled]) &&
|
||||
!Pleroma.Config.get([:instance, :registrations_open]),
|
||||
{:ok, invite_token} <- UserInviteToken.create_invite(),
|
||||
email <-
|
||||
Pleroma.Emails.UserEmail.user_invitation_email(
|
||||
user,
|
||||
invite_token,
|
||||
email,
|
||||
params["name"]
|
||||
),
|
||||
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
end
|
||||
|
||||
@shortdoc "Get a password reset token (base64 string) for given nickname"
|
||||
@doc "Get a account registeration invite token (base64 string)"
|
||||
def get_invite_token(conn, params) do
|
||||
options = params["invite"] || %{}
|
||||
{:ok, invite} = UserInviteToken.create_invite(options)
|
||||
|
||||
conn
|
||||
|> json(invite.token)
|
||||
end
|
||||
|
||||
@doc "Get list of created invites"
|
||||
def invites(conn, _params) do
|
||||
invites = UserInviteToken.list_invites()
|
||||
|
||||
conn
|
||||
|> json(AccountView.render("invites.json", %{invites: invites}))
|
||||
end
|
||||
|
||||
@doc "Revokes invite by token"
|
||||
def revoke_invite(conn, %{"token" => token}) do
|
||||
invite = UserInviteToken.find_by_token!(token)
|
||||
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true})
|
||||
|
||||
conn
|
||||
|> json(AccountView.render("invite.json", %{invite: updated_invite}))
|
||||
end
|
||||
|
||||
@doc "Get a password reset token (base64 string) for given nickname"
|
||||
def get_password_reset(conn, %{"nickname" => nickname}) do
|
||||
(%User{local: true} = user) = User.get_by_nickname(nickname)
|
||||
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
|
||||
|
|
@ -157,6 +284,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(token.token)
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json("Not found")
|
||||
end
|
||||
|
||||
def errors(conn, {:param_cast, _}) do
|
||||
conn
|
||||
|> put_status(400)
|
||||
|
|
@ -168,4 +301,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> put_status(500)
|
||||
|> json("Something went wrong")
|
||||
end
|
||||
|
||||
defp page_params(params) do
|
||||
{get_page(params["page"]), get_page_size(params["page_size"])}
|
||||
end
|
||||
|
||||
defp get_page(page_string) when is_nil(page_string), do: 1
|
||||
|
||||
defp get_page(page_string) do
|
||||
case Integer.parse(page_string) do
|
||||
{page, _} -> page
|
||||
:error -> 1
|
||||
end
|
||||
end
|
||||
|
||||
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
|
||||
|
||||
defp get_page_size(page_size_string) do
|
||||
case Integer.parse(page_size_string) do
|
||||
{page_size, _} -> page_size
|
||||
:error -> @users_page_size
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
54
lib/pleroma/web/admin_api/search.ex
Normal file
54
lib/pleroma/web/admin_api/search.ex
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.Search do
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@page_size 50
|
||||
|
||||
def user(%{query: term} = params) when is_nil(term) or term == "" do
|
||||
query = maybe_filtered_query(params)
|
||||
|
||||
paginated_query =
|
||||
maybe_filtered_query(params)
|
||||
|> paginate(params[:page] || 1, params[:page_size] || @page_size)
|
||||
|
||||
count = query |> Repo.aggregate(:count, :id)
|
||||
|
||||
results = Repo.all(paginated_query)
|
||||
|
||||
{:ok, results, count}
|
||||
end
|
||||
|
||||
def user(%{query: term} = params) when is_binary(term) do
|
||||
search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%"))
|
||||
|
||||
count = search_query |> Repo.aggregate(:count, :id)
|
||||
|
||||
results =
|
||||
search_query
|
||||
|> paginate(params[:page] || 1, params[:page_size] || @page_size)
|
||||
|> Repo.all()
|
||||
|
||||
{:ok, results, count}
|
||||
end
|
||||
|
||||
defp maybe_filtered_query(params) do
|
||||
from(u in User, order_by: u.nickname)
|
||||
|> User.maybe_local_user_query(params[:local])
|
||||
|> User.maybe_external_user_query(params[:external])
|
||||
|> User.maybe_active_user_query(params[:active])
|
||||
|> User.maybe_deactivated_user_query(params[:deactivated])
|
||||
end
|
||||
|
||||
defp paginate(query, page, page_size) do
|
||||
from(u in query,
|
||||
limit: ^page_size,
|
||||
offset: ^((page - 1) * page_size)
|
||||
)
|
||||
end
|
||||
end
|
||||
47
lib/pleroma/web/admin_api/views/account_view.ex
Normal file
47
lib/pleroma/web/admin_api/views/account_view.ex
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.AccountView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.User.Info
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
|
||||
def render("index.json", %{users: users, count: count, page_size: page_size}) do
|
||||
%{
|
||||
users: render_many(users, AccountView, "show.json", as: :user),
|
||||
count: count,
|
||||
page_size: page_size
|
||||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user}) do
|
||||
%{
|
||||
"id" => user.id,
|
||||
"nickname" => user.nickname,
|
||||
"deactivated" => user.info.deactivated,
|
||||
"local" => user.local,
|
||||
"roles" => Info.roles(user.info),
|
||||
"tags" => user.tags || []
|
||||
}
|
||||
end
|
||||
|
||||
def render("invite.json", %{invite: invite}) do
|
||||
%{
|
||||
"id" => invite.id,
|
||||
"token" => invite.token,
|
||||
"used" => invite.used,
|
||||
"expires_at" => invite.expires_at,
|
||||
"uses" => invite.uses,
|
||||
"max_use" => invite.max_use,
|
||||
"invite_type" => invite.invite_type
|
||||
}
|
||||
end
|
||||
|
||||
def render("invites.json", %{invites: invites}) do
|
||||
%{
|
||||
invites: render_many(invites, AccountView, "invite.json", as: :invite)
|
||||
}
|
||||
end
|
||||
end
|
||||
45
lib/pleroma/web/auth/authenticator.ex
Normal file
45
lib/pleroma/web/auth/authenticator.ex
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.Authenticator do
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.User
|
||||
|
||||
def implementation do
|
||||
Pleroma.Config.get(
|
||||
Pleroma.Web.Auth.Authenticator,
|
||||
Pleroma.Web.Auth.PleromaAuthenticator
|
||||
)
|
||||
end
|
||||
|
||||
@callback get_user(Plug.Conn.t(), Map.t()) :: {:ok, User.t()} | {:error, any()}
|
||||
def get_user(plug, params), do: implementation().get_user(plug, params)
|
||||
|
||||
@callback create_from_registration(Plug.Conn.t(), Map.t(), Registration.t()) ::
|
||||
{:ok, User.t()} | {:error, any()}
|
||||
def create_from_registration(plug, params, registration),
|
||||
do: implementation().create_from_registration(plug, params, registration)
|
||||
|
||||
@callback get_registration(Plug.Conn.t(), Map.t()) ::
|
||||
{:ok, Registration.t()} | {:error, any()}
|
||||
def get_registration(plug, params),
|
||||
do: implementation().get_registration(plug, params)
|
||||
|
||||
@callback handle_error(Plug.Conn.t(), any()) :: any()
|
||||
def handle_error(plug, error), do: implementation().handle_error(plug, error)
|
||||
|
||||
@callback auth_template() :: String.t() | nil
|
||||
def auth_template do
|
||||
# Note: `config :pleroma, :auth_template, "..."` support is deprecated
|
||||
implementation().auth_template() ||
|
||||
Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) ||
|
||||
"show.html"
|
||||
end
|
||||
|
||||
@callback oauth_consumer_template() :: String.t() | nil
|
||||
def oauth_consumer_template do
|
||||
implementation().oauth_consumer_template() ||
|
||||
Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html")
|
||||
end
|
||||
end
|
||||
150
lib/pleroma/web/auth/ldap_authenticator.ex
Normal file
150
lib/pleroma/web/auth/ldap_authenticator.ex
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
|
||||
alias Pleroma.User
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Pleroma.Web.Auth.Authenticator
|
||||
@base Pleroma.Web.Auth.PleromaAuthenticator
|
||||
|
||||
@connection_timeout 10_000
|
||||
@search_timeout 10_000
|
||||
|
||||
defdelegate get_registration(conn, params), to: @base
|
||||
|
||||
defdelegate create_from_registration(conn, params, registration), to: @base
|
||||
|
||||
def get_user(%Plug.Conn{} = conn, params) do
|
||||
if Pleroma.Config.get([:ldap, :enabled]) do
|
||||
{name, password} =
|
||||
case params do
|
||||
%{"authorization" => %{"name" => name, "password" => password}} ->
|
||||
{name, password}
|
||||
|
||||
%{"grant_type" => "password", "username" => name, "password" => password} ->
|
||||
{name, password}
|
||||
end
|
||||
|
||||
case ldap_user(name, password) do
|
||||
%User{} = user ->
|
||||
{:ok, user}
|
||||
|
||||
{:error, {:ldap_connection_error, _}} ->
|
||||
# When LDAP is unavailable, try default authenticator
|
||||
@base.get_user(conn, params)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
else
|
||||
# Fall back to default authenticator
|
||||
@base.get_user(conn, params)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_error(%Plug.Conn{} = _conn, error) do
|
||||
error
|
||||
end
|
||||
|
||||
def auth_template, do: nil
|
||||
|
||||
def oauth_consumer_template, do: nil
|
||||
|
||||
defp ldap_user(name, password) do
|
||||
ldap = Pleroma.Config.get(:ldap, [])
|
||||
host = Keyword.get(ldap, :host, "localhost")
|
||||
port = Keyword.get(ldap, :port, 389)
|
||||
ssl = Keyword.get(ldap, :ssl, false)
|
||||
sslopts = Keyword.get(ldap, :sslopts, [])
|
||||
|
||||
options =
|
||||
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
|
||||
if sslopts != [], do: [{:sslopts, sslopts}], else: []
|
||||
|
||||
case :eldap.open([to_charlist(host)], options) do
|
||||
{:ok, connection} ->
|
||||
try do
|
||||
if Keyword.get(ldap, :tls, false) do
|
||||
:application.ensure_all_started(:ssl)
|
||||
|
||||
case :eldap.start_tls(
|
||||
connection,
|
||||
Keyword.get(ldap, :tlsopts, []),
|
||||
@connection_timeout
|
||||
) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error("Could not start TLS: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
bind_user(connection, ldap, name, password)
|
||||
after
|
||||
:eldap.close(connection)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Could not open LDAP connection: #{inspect(error)}")
|
||||
{:error, {:ldap_connection_error, error}}
|
||||
end
|
||||
end
|
||||
|
||||
defp bind_user(connection, ldap, name, password) do
|
||||
uid = Keyword.get(ldap, :uid, "cn")
|
||||
base = Keyword.get(ldap, :base)
|
||||
|
||||
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
|
||||
:ok ->
|
||||
case User.get_by_nickname_or_email(name) do
|
||||
%User{} = user ->
|
||||
user
|
||||
|
||||
_ ->
|
||||
register_user(connection, base, uid, name, password)
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp register_user(connection, base, uid, name, password) do
|
||||
case :eldap.search(connection, [
|
||||
{:base, to_charlist(base)},
|
||||
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
|
||||
{:scope, :eldap.wholeSubtree()},
|
||||
{:attributes, ['mail', 'email']},
|
||||
{:timeout, @search_timeout}
|
||||
]) do
|
||||
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
|
||||
with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
|
||||
params = %{
|
||||
email: :erlang.list_to_binary(mail),
|
||||
name: name,
|
||||
nickname: name,
|
||||
password: password,
|
||||
password_confirmation: password
|
||||
}
|
||||
|
||||
changeset = User.register_changeset(%User{}, params)
|
||||
|
||||
case User.register(changeset) do
|
||||
{:ok, user} -> user
|
||||
error -> error
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
|
||||
{:error, :ldap_registration_missing_attributes}
|
||||
end
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
97
lib/pleroma/web/auth/pleroma_authenticator.ex
Normal file
97
lib/pleroma/web/auth/pleroma_authenticator.ex
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@behaviour Pleroma.Web.Auth.Authenticator
|
||||
|
||||
def get_user(%Plug.Conn{} = _conn, params) do
|
||||
{name, password} =
|
||||
case params do
|
||||
%{"authorization" => %{"name" => name, "password" => password}} ->
|
||||
{name, password}
|
||||
|
||||
%{"grant_type" => "password", "username" => name, "password" => password} ->
|
||||
{name, password}
|
||||
end
|
||||
|
||||
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
|
||||
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
|
||||
{:ok, user}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def get_registration(
|
||||
%Plug.Conn{assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}},
|
||||
_params
|
||||
) do
|
||||
registration = Registration.get_by_provider_uid(provider, uid)
|
||||
|
||||
if registration do
|
||||
{:ok, registration}
|
||||
else
|
||||
info = auth.info
|
||||
|
||||
Registration.changeset(%Registration{}, %{
|
||||
provider: to_string(provider),
|
||||
uid: to_string(uid),
|
||||
info: %{
|
||||
"nickname" => info.nickname,
|
||||
"email" => info.email,
|
||||
"name" => info.name,
|
||||
"description" => info.description
|
||||
}
|
||||
})
|
||||
|> Repo.insert()
|
||||
end
|
||||
end
|
||||
|
||||
def get_registration(%Plug.Conn{} = _conn, _params), do: {:error, :missing_credentials}
|
||||
|
||||
def create_from_registration(_conn, params, registration) do
|
||||
nickname = value([params["nickname"], Registration.nickname(registration)])
|
||||
email = value([params["email"], Registration.email(registration)])
|
||||
name = value([params["name"], Registration.name(registration)]) || nickname
|
||||
bio = value([params["bio"], Registration.description(registration)])
|
||||
|
||||
random_password = :crypto.strong_rand_bytes(64) |> Base.encode64()
|
||||
|
||||
with {:ok, new_user} <-
|
||||
User.register_changeset(
|
||||
%User{},
|
||||
%{
|
||||
email: email,
|
||||
nickname: nickname,
|
||||
name: name,
|
||||
bio: bio,
|
||||
password: random_password,
|
||||
password_confirmation: random_password
|
||||
},
|
||||
external: true,
|
||||
confirmed: true
|
||||
)
|
||||
|> Repo.insert(),
|
||||
{:ok, _} <-
|
||||
Registration.changeset(registration, %{user_id: new_user.id}) |> Repo.update() do
|
||||
{:ok, new_user}
|
||||
end
|
||||
end
|
||||
|
||||
defp value(list), do: Enum.find(list, &(to_string(&1) != ""))
|
||||
|
||||
def handle_error(%Plug.Conn{} = _conn, error) do
|
||||
error
|
||||
end
|
||||
|
||||
def auth_template, do: nil
|
||||
|
||||
def oauth_consumer_template, do: nil
|
||||
end
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.UserSocket do
|
||||
use Phoenix.Socket
|
||||
alias Pleroma.User
|
||||
|
|
@ -6,10 +10,6 @@ defmodule Pleroma.Web.UserSocket do
|
|||
# channel "room:*", Pleroma.Web.RoomChannel
|
||||
channel("chat:*", Pleroma.Web.ChatChannel)
|
||||
|
||||
## Transports
|
||||
transport(:websocket, Phoenix.Transports.WebSocket)
|
||||
# transport :longpoll, Phoenix.Transports.LongPoll
|
||||
|
||||
# Socket params are passed from the client and can
|
||||
# be used to verify and authenticate a user. After
|
||||
# verification, you can put default assigns into
|
||||
|
|
@ -23,8 +23,8 @@ defmodule Pleroma.Web.UserSocket do
|
|||
# performing token verification on connect.
|
||||
def connect(%{"token" => token}, socket) do
|
||||
with true <- Pleroma.Config.get([:chat, :enabled]),
|
||||
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
|
||||
%User{} = user <- Pleroma.Repo.get(User, user_id) do
|
||||
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
|
||||
%User{} = user <- Pleroma.User.get_by_id(user_id) do
|
||||
{:ok, assign(socket, :user_name, user.nickname)}
|
||||
else
|
||||
_e -> :error
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ChatChannel do
|
||||
use Phoenix.Channel
|
||||
alias Pleroma.Web.ChatChannel.ChatChannelState
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ChatChannel.ChatChannelState
|
||||
|
||||
def join("chat:public", _message, socket) do
|
||||
send(self(), :after_join)
|
||||
|
|
@ -44,7 +48,7 @@ defmodule Pleroma.Web.ChatChannel.ChatChannelState do
|
|||
end)
|
||||
end
|
||||
|
||||
def messages() do
|
||||
def messages do
|
||||
Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse() end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +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.CommonAPI do
|
||||
alias Pleroma.{User, Repo, Activity, Object}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
import Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
def follow(follower, followed) do
|
||||
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
|
||||
{:ok, activity} <- ActivityPub.follow(follower, followed),
|
||||
{:ok, follower, followed} <-
|
||||
User.wait_and_refresh(
|
||||
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
|
||||
follower,
|
||||
followed
|
||||
) do
|
||||
{:ok, follower, followed, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow(follower, unfollowed) do
|
||||
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
||||
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
||||
def accept_follow_request(follower, followed) do
|
||||
with {:ok, follower} <- User.maybe_follow(follower, followed),
|
||||
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
|
||||
{:ok, _activity} <-
|
||||
ActivityPub.accept(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
type: "Accept"
|
||||
}) do
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
||||
def reject_follow_request(follower, followed) do
|
||||
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
||||
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
|
||||
{:ok, _activity} <-
|
||||
ActivityPub.reject(%{
|
||||
to: [follower.ap_id],
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
type: "Reject"
|
||||
}) do
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(activity_id, user) do
|
||||
with %Activity{data: %{"object" => object_id}} <- Repo.get(Activity, activity_id),
|
||||
%Object{} = object <- Object.normalize(object_id),
|
||||
true <- user.info.is_moderator || user.ap_id == object.data["actor"],
|
||||
with %Activity{data: %{"object" => _}} = activity <-
|
||||
Activity.get_by_id_with_object(activity_id),
|
||||
%Object{} = object <- Object.normalize(activity),
|
||||
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
||||
{:ok, _} <- unpin(activity_id, user),
|
||||
{:ok, delete} <- ActivityPub.delete(object) do
|
||||
{:ok, delete}
|
||||
end
|
||||
|
|
@ -16,7 +75,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def repeat(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]) do
|
||||
object <- Object.normalize(activity),
|
||||
nil <- Utils.get_existing_announce(user.ap_id, object) do
|
||||
ActivityPub.announce(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -26,7 +86,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def unrepeat(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]) do
|
||||
object <- Object.normalize(activity) do
|
||||
ActivityPub.unannounce(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -36,7 +96,8 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def favorite(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]) do
|
||||
object <- Object.normalize(activity),
|
||||
nil <- Utils.get_existing_like(user.ap_id, object) do
|
||||
ActivityPub.like(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -46,7 +107,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def unfavorite(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
object <- Object.normalize(activity.data["object"]) do
|
||||
object <- Object.normalize(activity) do
|
||||
ActivityPub.unlike(user, object)
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -63,9 +124,9 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
nil ->
|
||||
"public"
|
||||
|
||||
inReplyTo ->
|
||||
in_reply_to ->
|
||||
# XXX: these heuristics should be moved out of MastodonAPI.
|
||||
with %Object{} = object <- Object.normalize(inReplyTo.data["object"]) do
|
||||
with %Object{} = object <- Object.normalize(in_reply_to) do
|
||||
Pleroma.Web.MastodonAPI.StatusView.get_visibility(object.data)
|
||||
end
|
||||
end
|
||||
|
|
@ -73,34 +134,22 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def get_visibility(_), do: "public"
|
||||
|
||||
defp get_content_type(content_type) do
|
||||
if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do
|
||||
content_type
|
||||
else
|
||||
"text/plain"
|
||||
end
|
||||
end
|
||||
|
||||
def post(user, %{"status" => status} = data) do
|
||||
visibility = get_visibility(data)
|
||||
limit = Pleroma.Config.get([:instance, :limit])
|
||||
|
||||
with status <- String.trim(status),
|
||||
attachments <- attachments_from_ids(data["media_ids"]),
|
||||
mentions <- Formatter.parse_mentions(status),
|
||||
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
|
||||
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
|
||||
tags <- Formatter.parse_tags(status, data),
|
||||
content_html <-
|
||||
attachments <- attachments_from_ids(data),
|
||||
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
|
||||
{content_html, mentions, tags} <-
|
||||
make_content_html(
|
||||
status,
|
||||
mentions,
|
||||
attachments,
|
||||
tags,
|
||||
get_content_type(data["content_type"]),
|
||||
data["no_attachment_links"]
|
||||
data,
|
||||
visibility
|
||||
),
|
||||
context <- make_context(inReplyTo),
|
||||
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
|
||||
context <- make_context(in_reply_to),
|
||||
cw <- data["spoiler_text"],
|
||||
full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
|
||||
length when length in 1..limit <- String.length(full_payload),
|
||||
|
|
@ -111,7 +160,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
context,
|
||||
content_html,
|
||||
attachments,
|
||||
inReplyTo,
|
||||
in_reply_to,
|
||||
tags,
|
||||
cw,
|
||||
cc
|
||||
|
|
@ -120,19 +169,22 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
Map.put(
|
||||
object,
|
||||
"emoji",
|
||||
Formatter.get_emoji(status)
|
||||
|> Enum.reduce(%{}, fn {name, file}, acc ->
|
||||
(Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"]))
|
||||
|> Enum.reduce(%{}, fn {name, file, _}, acc ->
|
||||
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
|
||||
end)
|
||||
) do
|
||||
res =
|
||||
ActivityPub.create(%{
|
||||
to: to,
|
||||
actor: user,
|
||||
context: context,
|
||||
object: object,
|
||||
additional: %{"cc" => cc}
|
||||
})
|
||||
ActivityPub.create(
|
||||
%{
|
||||
to: to,
|
||||
actor: user,
|
||||
context: context,
|
||||
object: object,
|
||||
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
|
||||
},
|
||||
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
|
||||
)
|
||||
|
||||
res
|
||||
end
|
||||
|
|
@ -160,4 +212,113 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
|
||||
})
|
||||
end
|
||||
|
||||
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
|
||||
with %Activity{
|
||||
actor: ^user_ap_id,
|
||||
data: %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"to" => object_to,
|
||||
"type" => "Note"
|
||||
}
|
||||
}
|
||||
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
|
||||
%{valid?: true} = info_changeset <-
|
||||
Pleroma.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
|
||||
{:ok, activity}
|
||||
else
|
||||
%{errors: [pinned_activities: {err, _}]} ->
|
||||
{:error, err}
|
||||
|
||||
_ ->
|
||||
{:error, "Could not pin"}
|
||||
end
|
||||
end
|
||||
|
||||
def unpin(id_or_ap_id, user) do
|
||||
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
||||
%{valid?: true} = info_changeset <-
|
||||
Pleroma.User.Info.remove_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
|
||||
{:ok, activity}
|
||||
else
|
||||
%{errors: [pinned_activities: {err, _}]} ->
|
||||
{:error, err}
|
||||
|
||||
_ ->
|
||||
{:error, "Could not unpin"}
|
||||
end
|
||||
end
|
||||
|
||||
def add_mute(user, activity) do
|
||||
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, _} -> {:error, "conversation is already muted"}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_mute(user, activity) do
|
||||
ThreadMute.remove_mute(user.id, activity.data["context"])
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
def thread_muted?(%{id: nil} = _user, _activity), do: false
|
||||
|
||||
def thread_muted?(user, activity) do
|
||||
with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
|
||||
false
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
def report(user, data) do
|
||||
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
|
||||
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
|
||||
{:ok, statuses} <- get_report_statuses(account, data),
|
||||
{:ok, activity} <-
|
||||
ActivityPub.flag(%{
|
||||
context: Utils.generate_context_id(),
|
||||
actor: user,
|
||||
account: account,
|
||||
statuses: statuses,
|
||||
content: content_html,
|
||||
forward: data["forward"] || false
|
||||
}) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:error, err} -> {:error, err}
|
||||
{:account_id, %{}} -> {:error, "Valid `account_id` required"}
|
||||
{:account, nil} -> {:error, "Account not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def hide_reblogs(user, muted) do
|
||||
ap_id = muted.ap_id
|
||||
|
||||
if ap_id not in user.info.muted_reblogs do
|
||||
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
|
||||
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
||||
User.update_and_set_cache(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def show_reblogs(user, muted) do
|
||||
ap_id = muted.ap_id
|
||||
|
||||
if ap_id in user.info.muted_reblogs do
|
||||
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
|
||||
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
||||
User.update_and_set_cache(changeset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,38 +1,66 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.CommonAPI.Utils do
|
||||
alias Pleroma.{Repo, Object, Formatter, Activity}
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.User
|
||||
alias Calendar.Strftime
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
require Logger
|
||||
|
||||
# This is a hack for twidere.
|
||||
def get_by_id_or_ap_id(id) do
|
||||
activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
|
||||
activity =
|
||||
Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
|
||||
|
||||
activity &&
|
||||
if activity.data["type"] == "Create" do
|
||||
activity
|
||||
else
|
||||
Activity.get_create_activity_by_object_ap_id(activity.data["object"])
|
||||
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
|
||||
end
|
||||
end
|
||||
|
||||
def get_replied_to_activity(""), do: nil
|
||||
|
||||
def get_replied_to_activity(id) when not is_nil(id) do
|
||||
Repo.get(Activity, id)
|
||||
Activity.get_by_id(id)
|
||||
end
|
||||
|
||||
def get_replied_to_activity(_), do: nil
|
||||
|
||||
def attachments_from_ids(ids) do
|
||||
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
|
||||
end
|
||||
|
||||
def attachments_from_ids_no_descs(ids) do
|
||||
Enum.map(ids || [], fn media_id ->
|
||||
Repo.get(Object, media_id).data
|
||||
end)
|
||||
end
|
||||
|
||||
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])
|
||||
end)
|
||||
end
|
||||
|
||||
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
|
||||
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
|
||||
|
||||
|
|
@ -76,24 +104,53 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
def make_content_html(
|
||||
status,
|
||||
mentions,
|
||||
attachments,
|
||||
tags,
|
||||
content_type,
|
||||
no_attachment_links \\ false
|
||||
data,
|
||||
visibility
|
||||
) do
|
||||
no_attachment_links =
|
||||
data
|
||||
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|
||||
|> Kernel.in([true, "true"])
|
||||
|
||||
content_type = get_content_type(data["content_type"])
|
||||
|
||||
options =
|
||||
if visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
|
||||
[safe_mention: true]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
status
|
||||
|> format_input(mentions, tags, content_type)
|
||||
|> format_input(content_type, options)
|
||||
|> maybe_add_attachments(attachments, no_attachment_links)
|
||||
|> maybe_add_nsfw_tag(data)
|
||||
end
|
||||
|
||||
defp get_content_type(content_type) do
|
||||
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
|
||||
content_type
|
||||
else
|
||||
"text/plain"
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
|
||||
when sensitive in [true, "True", "true", "1"] do
|
||||
{text, mentions, [{"#nsfw", "nsfw"} | tags]}
|
||||
end
|
||||
|
||||
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 maybe_add_attachments(text, _attachments, _no_links = true), do: text
|
||||
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
|
||||
|
||||
def maybe_add_attachments(text, attachments, _no_links) do
|
||||
add_attachments(text, attachments)
|
||||
def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
|
||||
text = add_attachments(text, attachments)
|
||||
{text, mentions, tags}
|
||||
end
|
||||
|
||||
def add_attachments(text, attachments) do
|
||||
|
|
@ -111,46 +168,38 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
Enum.join([text | attachment_text], "<br>")
|
||||
end
|
||||
|
||||
def format_input(text, mentions, tags, "text/plain") do
|
||||
def format_input(text, format, options \\ [])
|
||||
|
||||
@doc """
|
||||
Formatting text to plain text.
|
||||
"""
|
||||
def format_input(text, "text/plain", options) do
|
||||
text
|
||||
|> Formatter.html_escape("text/plain")
|
||||
|> String.replace(~r/\r?\n/, "<br>")
|
||||
|> (&{[], &1}).()
|
||||
|> Formatter.add_links()
|
||||
|> Formatter.add_user_links(mentions)
|
||||
|> Formatter.add_hashtag_links(tags)
|
||||
|> Formatter.finalize()
|
||||
|> Formatter.linkify(options)
|
||||
|> (fn {text, mentions, tags} ->
|
||||
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
|
||||
end).()
|
||||
end
|
||||
|
||||
def format_input(text, mentions, tags, "text/html") do
|
||||
@doc """
|
||||
Formatting text to html.
|
||||
"""
|
||||
def format_input(text, "text/html", options) do
|
||||
text
|
||||
|> Formatter.html_escape("text/html")
|
||||
|> String.replace(~r/\r?\n/, "<br>")
|
||||
|> (&{[], &1}).()
|
||||
|> Formatter.add_user_links(mentions)
|
||||
|> Formatter.finalize()
|
||||
|> Formatter.linkify(options)
|
||||
end
|
||||
|
||||
def format_input(text, mentions, tags, "text/markdown") do
|
||||
@doc """
|
||||
Formatting text to markdown.
|
||||
"""
|
||||
def format_input(text, "text/markdown", options) do
|
||||
text
|
||||
|> Formatter.mentions_escape(options)
|
||||
|> Earmark.as_html!()
|
||||
|> Formatter.linkify(options)
|
||||
|> Formatter.html_escape("text/html")
|
||||
|> String.replace(~r/\r?\n/, "")
|
||||
|> (&{[], &1}).()
|
||||
|> Formatter.add_user_links(mentions)
|
||||
|> Formatter.add_hashtag_links(tags)
|
||||
|> Formatter.finalize()
|
||||
end
|
||||
|
||||
def add_tag_links(text, tags) do
|
||||
tags =
|
||||
tags
|
||||
|> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
|
||||
|
||||
Enum.reduce(tags, text, fn {full, tag}, text ->
|
||||
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
|
||||
String.replace(text, full, url)
|
||||
end)
|
||||
end
|
||||
|
||||
def make_note_data(
|
||||
|
|
@ -181,7 +230,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
object
|
||||
|> Map.put("inReplyTo", inReplyToObject.data["id"])
|
||||
|> Map.put("inReplyToStatusId", inReplyTo.id)
|
||||
else
|
||||
object
|
||||
end
|
||||
|
|
@ -195,15 +243,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
|
||||
end
|
||||
|
||||
def date_to_asctime(date) do
|
||||
with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
|
||||
def date_to_asctime(date) when is_binary(date) do
|
||||
with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
|
||||
format_asctime(date)
|
||||
else
|
||||
_e ->
|
||||
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def date_to_asctime(date) do
|
||||
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
|
||||
""
|
||||
end
|
||||
|
||||
def to_masto_date(%NaiveDateTime{} = date) do
|
||||
date
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
|
@ -230,7 +284,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
|
||||
def confirm_current_password(user, password) do
|
||||
with %User{local: true} = db_user <- Repo.get(User, user.id),
|
||||
with %User{local: true} = db_user <- User.get_by_id(user.id),
|
||||
true <- Pbkdf2.checkpw(password, db_user.password_hash) do
|
||||
{:ok, db_user}
|
||||
else
|
||||
|
|
@ -238,9 +292,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
def emoji_from_profile(%{info: info} = user) do
|
||||
def emoji_from_profile(%{info: _info} = user) do
|
||||
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|
||||
|> Enum.map(fn {shortcode, url} ->
|
||||
|> Enum.map(fn {shortcode, url, _} ->
|
||||
%{
|
||||
"type" => "Emoji",
|
||||
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
|
||||
|
|
@ -248,4 +302,111 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
}
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_notify_to_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => to, "type" => _type}} = _activity
|
||||
) do
|
||||
recipients ++ to
|
||||
end
|
||||
|
||||
def maybe_notify_mentioned_recipients(
|
||||
recipients,
|
||||
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
|
||||
)
|
||||
when type == "Create" do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
object_data =
|
||||
cond do
|
||||
!is_nil(object) ->
|
||||
object.data
|
||||
|
||||
is_map(data["object"]) ->
|
||||
data["object"]
|
||||
|
||||
true ->
|
||||
%{}
|
||||
end
|
||||
|
||||
tagged_mentions = maybe_extract_mentions(object_data)
|
||||
|
||||
recipients ++ tagged_mentions
|
||||
end
|
||||
|
||||
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
|
||||
|
||||
def maybe_notify_subscribers(
|
||||
recipients,
|
||||
%Activity{data: %{"actor" => actor, "type" => type}} = activity
|
||||
)
|
||||
when type == "Create" do
|
||||
with %User{} = user <- User.get_cached_by_ap_id(actor) do
|
||||
subscriber_ids =
|
||||
user
|
||||
|> User.subscribers()
|
||||
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|
||||
|> Enum.map(& &1.ap_id)
|
||||
|
||||
recipients ++ subscriber_ids
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_notify_subscribers(recipients, _), do: recipients
|
||||
|
||||
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.map(fn x -> x["href"] end)
|
||||
end
|
||||
|
||||
def maybe_extract_mentions(_), do: []
|
||||
|
||||
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
|
||||
|
||||
def make_report_content_html(comment) do
|
||||
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
|
||||
|
||||
if String.length(comment) <= max_size do
|
||||
{:ok, format_input(comment, "text/plain")}
|
||||
else
|
||||
{:error, "Comment must be up to #{max_size} characters"}
|
||||
end
|
||||
end
|
||||
|
||||
def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
|
||||
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
|
||||
end
|
||||
|
||||
def get_report_statuses(_, _), do: {:ok, nil}
|
||||
|
||||
# DEPRECATED mostly, context objects are now created at insertion time.
|
||||
def context_to_conversation_id(context) do
|
||||
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
|
||||
id
|
||||
else
|
||||
_e ->
|
||||
changeset = Object.context_mapping(context)
|
||||
|
||||
case Repo.insert(changeset) do
|
||||
{:ok, %{id: id}} ->
|
||||
id
|
||||
|
||||
# This should be solved by an upsert, but it seems ecto
|
||||
# has problems accessing the constraint inside the jsonb.
|
||||
{:error, _} ->
|
||||
Object.get_cached_by_ap_id(context).id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def conversation_id_to_context(id) do
|
||||
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
|
||||
context
|
||||
else
|
||||
_e ->
|
||||
{:error, "No such conversation"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
24
lib/pleroma/web/controller_helper.ex
Normal file
24
lib/pleroma/web/controller_helper.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ControllerHelper do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
|
||||
@falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
|
||||
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
|
||||
def truthy_param?(value), do: value not in @falsy_param_values
|
||||
|
||||
def oauth_scopes(params, default) do
|
||||
# Note: `scopes` is used by Mastodon — supporting it but sticking to
|
||||
# OAuth's standard `scope` wherever we control it
|
||||
Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default)
|
||||
end
|
||||
|
||||
def json_response(conn, status, json) do
|
||||
conn
|
||||
|> put_status(status)
|
||||
|> json(json)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :pleroma
|
||||
|
||||
socket("/socket", Pleroma.Web.UserSocket)
|
||||
|
||||
socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phoenix.digest
|
||||
|
|
@ -14,12 +16,17 @@ defmodule Pleroma.Web.Endpoint do
|
|||
|
||||
plug(Pleroma.Plugs.UploadedMedia)
|
||||
|
||||
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
|
||||
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
|
||||
plug(Pleroma.Plugs.InstanceStatic, at: "/")
|
||||
|
||||
plug(
|
||||
Plug.Static,
|
||||
at: "/",
|
||||
from: :pleroma,
|
||||
only:
|
||||
~w(index.html static finmoji emoji packs sounds images instance sw.js favicon.png schemas)
|
||||
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
|
||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||
)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
|
|
@ -44,11 +51,17 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Plug.MethodOverride)
|
||||
plug(Plug.Head)
|
||||
|
||||
secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag])
|
||||
|
||||
cookie_name =
|
||||
if Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
|
||||
if secure_cookies,
|
||||
do: "__Host-pleroma_key",
|
||||
else: "pleroma_key"
|
||||
|
||||
extra =
|
||||
Pleroma.Config.get([__MODULE__, :extra_cookie_attrs])
|
||||
|> Enum.join(";")
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
# Set :encryption_salt if you would also like to encrypt it.
|
||||
|
|
@ -58,11 +71,30 @@ defmodule Pleroma.Web.Endpoint do
|
|||
key: cookie_name,
|
||||
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
|
||||
http_only: true,
|
||||
secure:
|
||||
Application.get_env(:pleroma, Pleroma.Web.Endpoint) |> Keyword.get(:secure_cookie_flag),
|
||||
extra: "SameSite=Strict"
|
||||
secure: secure_cookies,
|
||||
extra: extra
|
||||
)
|
||||
|
||||
# Note: the plug and its configuration is compile-time this can't be upstreamed yet
|
||||
if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
|
||||
plug(RemoteIp, proxies: proxies)
|
||||
end
|
||||
|
||||
defmodule Instrumenter do
|
||||
use Prometheus.PhoenixInstrumenter
|
||||
end
|
||||
|
||||
defmodule PipelineInstrumenter do
|
||||
use Prometheus.PlugPipelineInstrumenter
|
||||
end
|
||||
|
||||
defmodule MetricsExporter do
|
||||
use Prometheus.PlugExporter
|
||||
end
|
||||
|
||||
plug(PipelineInstrumenter)
|
||||
plug(MetricsExporter)
|
||||
|
||||
plug(Pleroma.Web.Router)
|
||||
|
||||
@doc """
|
||||
|
|
@ -76,4 +108,8 @@ defmodule Pleroma.Web.Endpoint do
|
|||
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
|
||||
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
|
||||
end
|
||||
|
||||
def websocket_url do
|
||||
String.replace_leading(url(), "http", "ws")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,55 +1,85 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Federator do
|
||||
use GenServer
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Web.{WebFinger, Websub}
|
||||
alias Pleroma.Web.Federator.RetryQueue
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator.RetryQueue
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Web.Salmon
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Pleroma.Web.Websub
|
||||
|
||||
require Logger
|
||||
|
||||
@websub Application.get_env(:pleroma, :websub)
|
||||
@ostatus Application.get_env(:pleroma, :ostatus)
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
@max_jobs 20
|
||||
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
def init do
|
||||
# 1 minute
|
||||
Process.sleep(1000 * 60)
|
||||
refresh_subscriptions()
|
||||
end
|
||||
|
||||
def start_link do
|
||||
spawn(fn ->
|
||||
# 1 minute
|
||||
Process.sleep(1000 * 60 * 1)
|
||||
enqueue(:refresh_subscriptions, nil)
|
||||
end)
|
||||
# Client API
|
||||
|
||||
GenServer.start_link(
|
||||
__MODULE__,
|
||||
%{
|
||||
in: {:sets.new(), []},
|
||||
out: {:sets.new(), []}
|
||||
},
|
||||
name: __MODULE__
|
||||
)
|
||||
def incoming_doc(doc) do
|
||||
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_doc, doc])
|
||||
end
|
||||
|
||||
def handle(:refresh_subscriptions, _) do
|
||||
def incoming_ap_doc(params) do
|
||||
PleromaJobQueue.enqueue(:federator_incoming, __MODULE__, [:incoming_ap_doc, params])
|
||||
end
|
||||
|
||||
def publish(activity, priority \\ 1) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority)
|
||||
end
|
||||
|
||||
def publish_single_ap(params) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params])
|
||||
end
|
||||
|
||||
def publish_single_websub(websub) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub])
|
||||
end
|
||||
|
||||
def verify_websub(websub) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub])
|
||||
end
|
||||
|
||||
def request_subscription(sub) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
|
||||
end
|
||||
|
||||
def refresh_subscriptions do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
|
||||
end
|
||||
|
||||
def publish_single_salmon(params) do
|
||||
PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params])
|
||||
end
|
||||
|
||||
# Job Worker Callbacks
|
||||
|
||||
def perform(:refresh_subscriptions) do
|
||||
Logger.debug("Federator running refresh subscriptions")
|
||||
Websub.refresh_subscriptions()
|
||||
|
||||
spawn(fn ->
|
||||
# 6 hours
|
||||
Process.sleep(1000 * 60 * 60 * 6)
|
||||
enqueue(:refresh_subscriptions, nil)
|
||||
refresh_subscriptions()
|
||||
end)
|
||||
end
|
||||
|
||||
def handle(:request_subscription, websub) do
|
||||
def perform(:request_subscription, websub) do
|
||||
Logger.debug("Refreshing #{websub.topic}")
|
||||
|
||||
with {:ok, websub} <- Websub.request_subscription(websub) do
|
||||
|
|
@ -59,13 +89,13 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(:publish, activity) do
|
||||
def perform(:publish, activity) do
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
||||
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
{:ok, actor} = WebFinger.ensure_keys_present(actor)
|
||||
|
||||
if ActivityPub.is_public?(activity) do
|
||||
if Visibility.is_public?(activity) do
|
||||
if OStatus.is_representable?(activity) do
|
||||
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
|
||||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
||||
|
|
@ -85,7 +115,7 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(:verify_websub, websub) do
|
||||
def perform(:verify_websub, websub) do
|
||||
Logger.debug(fn ->
|
||||
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
|
||||
end)
|
||||
|
|
@ -93,12 +123,12 @@ defmodule Pleroma.Web.Federator do
|
|||
@websub.verify(websub)
|
||||
end
|
||||
|
||||
def handle(:incoming_doc, doc) do
|
||||
def perform(:incoming_doc, doc) do
|
||||
Logger.info("Got document, trying to parse")
|
||||
@ostatus.handle_incoming(doc)
|
||||
end
|
||||
|
||||
def handle(:incoming_ap_doc, params) do
|
||||
def perform(:incoming_ap_doc, params) do
|
||||
Logger.info("Handling incoming AP activity")
|
||||
|
||||
params = Utils.normalize_params(params)
|
||||
|
|
@ -123,7 +153,11 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(:publish_single_ap, params) do
|
||||
def perform(:publish_single_salmon, params) do
|
||||
Salmon.send_to_user(params)
|
||||
end
|
||||
|
||||
def perform(:publish_single_ap, params) do
|
||||
case ActivityPub.publish_one(params) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
|
@ -133,9 +167,9 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(
|
||||
def perform(
|
||||
:publish_single_websub,
|
||||
%{xml: xml, topic: topic, callback: callback, secret: secret} = params
|
||||
%{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params
|
||||
) do
|
||||
case Websub.publish_one(params) do
|
||||
{:ok, _} ->
|
||||
|
|
@ -146,75 +180,11 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(type, _) do
|
||||
def perform(type, _) do
|
||||
Logger.debug(fn -> "Unknown task: #{type}" end)
|
||||
{:error, "Don't know what to do with this"}
|
||||
end
|
||||
|
||||
if Mix.env() == :test do
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
if Pleroma.Config.get([:instance, :federating]) do
|
||||
handle(type, payload)
|
||||
end
|
||||
end
|
||||
else
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
if Pleroma.Config.get([:instance, :federating]) do
|
||||
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_start_job(running_jobs, queue) do
|
||||
if :sets.size(running_jobs) < @max_jobs && queue != [] do
|
||||
{{type, payload}, queue} = queue_pop(queue)
|
||||
{:ok, pid} = Task.start(fn -> handle(type, payload) end)
|
||||
mref = Process.monitor(pid)
|
||||
{:sets.add_element(mref, running_jobs), queue}
|
||||
else
|
||||
{running_jobs, queue}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state)
|
||||
when type in [:incoming_doc, :incoming_ap_doc] do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
|
||||
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
|
||||
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def handle_cast(m, state) do
|
||||
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_running_jobs = :sets.del_element(ref, i_running_jobs)
|
||||
o_running_jobs = :sets.del_element(ref, o_running_jobs)
|
||||
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
|
||||
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
|
||||
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
def enqueue_sorted(queue, element, priority) do
|
||||
[%{item: element, priority: priority} | queue]
|
||||
|> Enum.sort_by(fn %{priority: priority} -> priority end)
|
||||
end
|
||||
|
||||
def queue_pop([%{item: element} | queue]) do
|
||||
{element, queue}
|
||||
end
|
||||
|
||||
def ap_enabled_actor(id) do
|
||||
user = User.get_by_ap_id(id)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,171 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Federator.RetryQueue do
|
||||
use GenServer
|
||||
alias Pleroma.Web.{WebFinger, Websub}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
require Logger
|
||||
|
||||
@websub Application.get_env(:pleroma, :websub)
|
||||
@ostatus Application.get_env(:pleroma, :websub)
|
||||
@httpoison Application.get_env(:pleroma, :websub)
|
||||
@instance Application.get_env(:pleroma, :websub)
|
||||
# initial timeout, 5 min
|
||||
@initial_timeout 30_000
|
||||
@max_retries 5
|
||||
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
queue_table = :ets.new(:pleroma_retry_queue, [:bag, :protected])
|
||||
|
||||
{:ok, %{args | queue_table: queue_table, running_jobs: :sets.new()}}
|
||||
end
|
||||
|
||||
def start_link() do
|
||||
GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
|
||||
def start_link do
|
||||
enabled =
|
||||
if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false)
|
||||
|
||||
if enabled do
|
||||
Logger.info("Starting retry queue")
|
||||
|
||||
linkres =
|
||||
GenServer.start_link(
|
||||
__MODULE__,
|
||||
%{delivered: 0, dropped: 0, queue_table: nil, running_jobs: nil},
|
||||
name: __MODULE__
|
||||
)
|
||||
|
||||
maybe_kickoff_timer()
|
||||
linkres
|
||||
else
|
||||
Logger.info("Retry queue disabled")
|
||||
:ignore
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue(data, transport, retries \\ 0) do
|
||||
GenServer.cast(__MODULE__, {:maybe_enqueue, data, transport, retries + 1})
|
||||
end
|
||||
|
||||
def get_stats do
|
||||
GenServer.call(__MODULE__, :get_stats)
|
||||
end
|
||||
|
||||
def reset_stats do
|
||||
GenServer.call(__MODULE__, :reset_stats)
|
||||
end
|
||||
|
||||
def get_retry_params(retries) do
|
||||
if retries > @max_retries do
|
||||
if retries > Pleroma.Config.get([__MODULE__, :max_retries]) do
|
||||
{:drop, "Max retries reached"}
|
||||
else
|
||||
{:retry, growth_function(retries)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:maybe_enqueue, data, transport, retries}, %{dropped: drop_count} = state) do
|
||||
def get_retry_timer_interval do
|
||||
Pleroma.Config.get([:retry_queue, :interval], 1000)
|
||||
end
|
||||
|
||||
defp ets_count_expires(table, current_time) do
|
||||
:ets.select_count(
|
||||
table,
|
||||
[
|
||||
{
|
||||
{:"$1", :"$2"},
|
||||
[{:"=<", :"$1", {:const, current_time}}],
|
||||
[true]
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp ets_pop_n_expired(table, current_time, desired) do
|
||||
{popped, _continuation} =
|
||||
:ets.select(
|
||||
table,
|
||||
[
|
||||
{
|
||||
{:"$1", :"$2"},
|
||||
[{:"=<", :"$1", {:const, current_time}}],
|
||||
[:"$_"]
|
||||
}
|
||||
],
|
||||
desired
|
||||
)
|
||||
|
||||
popped
|
||||
|> Enum.each(fn e ->
|
||||
:ets.delete_object(table, e)
|
||||
end)
|
||||
|
||||
popped
|
||||
end
|
||||
|
||||
def maybe_start_job(running_jobs, queue_table) do
|
||||
# we don't want to hit the ets or the DateTime more times than we have to
|
||||
# could optimize slightly further by not using the count, and instead grabbing
|
||||
# up to N objects early...
|
||||
current_time = DateTime.to_unix(DateTime.utc_now())
|
||||
n_running_jobs = :sets.size(running_jobs)
|
||||
|
||||
if n_running_jobs < Pleroma.Config.get([__MODULE__, :max_jobs]) do
|
||||
n_ready_jobs = ets_count_expires(queue_table, current_time)
|
||||
|
||||
if n_ready_jobs > 0 do
|
||||
# figure out how many we could start
|
||||
available_job_slots = Pleroma.Config.get([__MODULE__, :max_jobs]) - n_running_jobs
|
||||
start_n_jobs(running_jobs, queue_table, current_time, available_job_slots)
|
||||
else
|
||||
running_jobs
|
||||
end
|
||||
else
|
||||
running_jobs
|
||||
end
|
||||
end
|
||||
|
||||
defp start_n_jobs(running_jobs, _queue_table, _current_time, 0) do
|
||||
running_jobs
|
||||
end
|
||||
|
||||
defp start_n_jobs(running_jobs, queue_table, current_time, available_job_slots)
|
||||
when available_job_slots > 0 do
|
||||
candidates = ets_pop_n_expired(queue_table, current_time, available_job_slots)
|
||||
|
||||
candidates
|
||||
|> List.foldl(running_jobs, fn {_, e}, rj ->
|
||||
{:ok, pid} = Task.start(fn -> worker(e) end)
|
||||
mref = Process.monitor(pid)
|
||||
:sets.add_element(mref, rj)
|
||||
end)
|
||||
end
|
||||
|
||||
def worker({:send, data, transport, retries}) do
|
||||
case transport.publish_one(data) do
|
||||
{:ok, _} ->
|
||||
GenServer.cast(__MODULE__, :inc_delivered)
|
||||
:delivered
|
||||
|
||||
{:error, _reason} ->
|
||||
enqueue(data, transport, retries)
|
||||
:retry
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:get_stats, _from, %{delivered: delivery_count, dropped: drop_count} = state) do
|
||||
{:reply, %{delivered: delivery_count, dropped: drop_count}, state}
|
||||
end
|
||||
|
||||
def handle_call(:reset_stats, _from, %{delivered: delivery_count, dropped: drop_count} = state) do
|
||||
{:reply, %{delivered: delivery_count, dropped: drop_count},
|
||||
%{state | delivered: 0, dropped: 0}}
|
||||
end
|
||||
|
||||
def handle_cast(:reset_stats, state) do
|
||||
{:noreply, %{state | delivered: 0, dropped: 0}}
|
||||
end
|
||||
|
||||
def handle_cast(
|
||||
{:maybe_enqueue, data, transport, retries},
|
||||
%{dropped: drop_count, queue_table: queue_table, running_jobs: running_jobs} = state
|
||||
) do
|
||||
case get_retry_params(retries) do
|
||||
{:retry, timeout} ->
|
||||
Process.send_after(
|
||||
__MODULE__,
|
||||
{:send, data, transport, retries},
|
||||
growth_function(retries)
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
:ets.insert(queue_table, {timeout, {:send, data, transport, retries}})
|
||||
running_jobs = maybe_start_job(running_jobs, queue_table)
|
||||
{:noreply, %{state | running_jobs: running_jobs}}
|
||||
|
||||
{:drop, message} ->
|
||||
Logger.debug(message)
|
||||
|
|
@ -49,23 +173,65 @@ defmodule Pleroma.Web.Federator.RetryQueue do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_cast(:kickoff_timer, state) do
|
||||
retry_interval = get_retry_timer_interval()
|
||||
Process.send_after(__MODULE__, :retry_timer_run, retry_interval)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(:inc_delivered, %{delivered: delivery_count} = state) do
|
||||
{:noreply, %{state | delivered: delivery_count + 1}}
|
||||
end
|
||||
|
||||
def handle_cast(:inc_dropped, %{dropped: drop_count} = state) do
|
||||
{:noreply, %{state | dropped: drop_count + 1}}
|
||||
end
|
||||
|
||||
def handle_info({:send, data, transport, retries}, %{delivered: delivery_count} = state) do
|
||||
case transport.publish_one(data) do
|
||||
{:ok, _} ->
|
||||
{:noreply, %{state | delivered: delivery_count + 1}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, _reason} ->
|
||||
enqueue(data, transport, retries)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:retry_timer_run,
|
||||
%{queue_table: queue_table, running_jobs: running_jobs} = state
|
||||
) do
|
||||
maybe_kickoff_timer()
|
||||
running_jobs = maybe_start_job(running_jobs, queue_table)
|
||||
{:noreply, %{state | running_jobs: running_jobs}}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
|
||||
%{running_jobs: running_jobs, queue_table: queue_table} = state
|
||||
running_jobs = :sets.del_element(ref, running_jobs)
|
||||
running_jobs = maybe_start_job(running_jobs, queue_table)
|
||||
{:noreply, %{state | running_jobs: running_jobs}}
|
||||
end
|
||||
|
||||
def handle_info(unknown, state) do
|
||||
Logger.debug("RetryQueue: don't know what to do with #{inspect(unknown)}, ignoring")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp growth_function(retries) do
|
||||
round(@initial_timeout * :math.pow(retries, 3))
|
||||
if Mix.env() == :test do
|
||||
defp growth_function(_retries) do
|
||||
_shutit = Pleroma.Config.get([__MODULE__, :initial_timeout])
|
||||
DateTime.to_unix(DateTime.utc_now()) - 1
|
||||
end
|
||||
else
|
||||
defp growth_function(retries) do
|
||||
round(Pleroma.Config.get([__MODULE__, :initial_timeout]) * :math.pow(retries, 3)) +
|
||||
DateTime.to_unix(DateTime.utc_now())
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_kickoff_timer do
|
||||
GenServer.cast(__MODULE__, :kickoff_timer)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Gettext do
|
||||
@moduledoc """
|
||||
A module providing Internationalization with a gettext-based API.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-08
|
||||
defmodule Pleroma.Web.HTTPSignatures do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
require Logger
|
||||
|
||||
def split_signature(sig) do
|
||||
|
|
|
|||
|
|
@ -1 +1,58 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.ScheduledActivity
|
||||
alias Pleroma.User
|
||||
|
||||
def get_followers(user, params \\ %{}) do
|
||||
user
|
||||
|> User.get_followers_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
def get_friends(user, params \\ %{}) do
|
||||
user
|
||||
|> User.get_friends_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
def get_notifications(user, params \\ %{}) do
|
||||
options = cast_params(params)
|
||||
|
||||
user
|
||||
|> Notification.for_user_query()
|
||||
|> restrict(:exclude_types, options)
|
||||
|> Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
def get_scheduled_activities(user, params \\ %{}) do
|
||||
user
|
||||
|> ScheduledActivity.for_user_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
end
|
||||
|
||||
defp cast_params(params) do
|
||||
param_types = %{
|
||||
exclude_types: {:array, :string}
|
||||
}
|
||||
|
||||
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
|
||||
changeset.changes
|
||||
end
|
||||
|
||||
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
|
||||
ap_types =
|
||||
mastodon_types
|
||||
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
query
|
||||
|> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
end
|
||||
|
||||
defp restrict(query, _, _), do: query
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,80 +0,0 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.{User, Repo}
|
||||
|
||||
transport(
|
||||
:streaming,
|
||||
Phoenix.Transports.WebSocket.Raw,
|
||||
# We never receive data.
|
||||
timeout: :infinity
|
||||
)
|
||||
|
||||
def connect(%{"access_token" => token} = params, socket) do
|
||||
with %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
|
||||
%User{} = user <- Repo.get(User, user_id),
|
||||
stream
|
||||
when stream in [
|
||||
"public",
|
||||
"public:local",
|
||||
"public:media",
|
||||
"public:local:media",
|
||||
"user",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
] <- params["stream"] do
|
||||
topic =
|
||||
case stream do
|
||||
"hashtag" -> "hashtag:#{params["tag"]}"
|
||||
"list" -> "list:#{params["list"]}"
|
||||
_ -> stream
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:topic, topic)
|
||||
|> assign(:user, user)
|
||||
|
||||
Pleroma.Web.Streamer.add_socket(topic, socket)
|
||||
{:ok, socket}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def connect(%{"stream" => stream} = params, socket)
|
||||
when stream in ["public", "public:local", "hashtag"] do
|
||||
topic =
|
||||
case stream do
|
||||
"hashtag" -> "hashtag:#{params["tag"]}"
|
||||
_ -> stream
|
||||
end
|
||||
|
||||
with socket =
|
||||
socket
|
||||
|> assign(:topic, topic) do
|
||||
Pleroma.Web.Streamer.add_socket(topic, socket)
|
||||
{:ok, socket}
|
||||
else
|
||||
_e -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def id(_), do: nil
|
||||
|
||||
def handle(:text, message, _state) do
|
||||
# | :ok
|
||||
# | state
|
||||
# | {:text, message}
|
||||
# | {:text, message, state}
|
||||
# | {:close, "Goodbye!"}
|
||||
{:text, message}
|
||||
end
|
||||
|
||||
def handle(:closed, _, %{socket: socket}) do
|
||||
topic = socket.assigns[:topic]
|
||||
Pleroma.Web.Streamer.remove_socket(topic, socket)
|
||||
end
|
||||
end
|
||||
71
lib/pleroma/web/mastodon_api/subscription_controller.ex
Normal file
71
lib/pleroma/web/mastodon_api/subscription_controller.ex
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
|
||||
@moduledoc "The module represents functions to manage user subscriptions."
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
|
||||
|
||||
action_fallback(:errors)
|
||||
|
||||
# Creates PushSubscription
|
||||
# POST /api/v1/push/subscription
|
||||
#
|
||||
def create(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, _} <- Subscription.delete_if_exists(user, token),
|
||||
{:ok, subscription} <- Subscription.create(user, token, params) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
end
|
||||
|
||||
# Gets PushSubscription
|
||||
# GET /api/v1/push/subscription
|
||||
#
|
||||
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, subscription} <- Subscription.get(user, token) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
end
|
||||
|
||||
# Updates PushSubscription
|
||||
# PUT /api/v1/push/subscription
|
||||
#
|
||||
def update(%{assigns: %{user: user, token: token}} = conn, params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, subscription} <- Subscription.update(user, token, params) do
|
||||
view = View.render("push_subscription.json", subscription: subscription)
|
||||
json(conn, view)
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes PushSubscription
|
||||
# DELETE /api/v1/push/subscription
|
||||
#
|
||||
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
|
||||
with true <- Push.enabled(),
|
||||
{:ok, _response} <- Subscription.delete(user, token),
|
||||
do: json(conn, %{})
|
||||
end
|
||||
|
||||
# fallback action
|
||||
#
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> json("Not found")
|
||||
end
|
||||
|
||||
def errors(conn, _) do
|
||||
conn
|
||||
|> put_status(500)
|
||||
|> json("Something went wrong")
|
||||
end
|
||||
end
|
||||
|
|
@ -1,16 +1,71 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.AccountView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def render("accounts.json", %{users: users} = opts) do
|
||||
render_many(users, AccountView, "account.json", opts)
|
||||
users
|
||||
|> render_many(AccountView, "account.json", opts)
|
||||
|> Enum.filter(&Enum.any?/1)
|
||||
end
|
||||
|
||||
def render("account.json", %{user: user} = opts) do
|
||||
if User.visible_for?(user, opts[:for]),
|
||||
do: do_render("account.json", opts),
|
||||
else: %{}
|
||||
end
|
||||
|
||||
def render("mention.json", %{user: user}) do
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
acct: user.nickname,
|
||||
username: username_from_nickname(user.nickname),
|
||||
url: user.ap_id
|
||||
}
|
||||
end
|
||||
|
||||
def render("relationship.json", %{user: nil, target: _target}) do
|
||||
%{}
|
||||
end
|
||||
|
||||
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
|
||||
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
|
||||
|
||||
requested =
|
||||
if follow_activity do
|
||||
follow_activity.data["state"] == "pending"
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
%{
|
||||
id: to_string(target.id),
|
||||
following: User.following?(user, target),
|
||||
followed_by: User.following?(target, user),
|
||||
blocking: User.blocks?(user, target),
|
||||
muting: User.mutes?(user, target),
|
||||
muting_notifications: false,
|
||||
subscribing: User.subscribed_to?(user, target),
|
||||
requested: requested,
|
||||
domain_blocking: false,
|
||||
showing_reblogs: User.showing_reblogs?(user, target),
|
||||
endorsed: false
|
||||
}
|
||||
end
|
||||
|
||||
def render("relationships.json", %{user: user, targets: targets}) do
|
||||
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
|
||||
end
|
||||
|
||||
defp do_render("account.json", %{user: user} = opts) do
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
user_info = User.user_info(user)
|
||||
|
|
@ -35,6 +90,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
|
||||
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
|
||||
|
||||
relationship = render("relationship.json", %{user: opts[:for], target: user})
|
||||
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
username: username_from_nickname(user.nickname),
|
||||
|
|
@ -58,50 +115,30 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
note: "",
|
||||
privacy: user_info.default_scope,
|
||||
sensitive: false
|
||||
}
|
||||
},
|
||||
|
||||
# Pleroma extension
|
||||
pleroma:
|
||||
%{
|
||||
confirmation_pending: user_info.confirmation_pending,
|
||||
tags: user.tags,
|
||||
is_moderator: user.info.is_moderator,
|
||||
is_admin: user.info.is_admin,
|
||||
relationship: relationship
|
||||
}
|
||||
|> with_notification_settings(user, opts[:for])
|
||||
}
|
||||
end
|
||||
|
||||
def render("mention.json", %{user: user}) do
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
acct: user.nickname,
|
||||
username: username_from_nickname(user.nickname),
|
||||
url: user.ap_id
|
||||
}
|
||||
end
|
||||
|
||||
def render("relationship.json", %{user: user, target: target}) do
|
||||
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
|
||||
|
||||
requested =
|
||||
if follow_activity do
|
||||
follow_activity.data["state"] == "pending"
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
%{
|
||||
id: to_string(target.id),
|
||||
following: User.following?(user, target),
|
||||
followed_by: User.following?(target, user),
|
||||
blocking: User.blocks?(user, target),
|
||||
muting: false,
|
||||
muting_notifications: false,
|
||||
requested: requested,
|
||||
domain_blocking: false,
|
||||
showing_reblogs: false,
|
||||
endorsed: false
|
||||
}
|
||||
end
|
||||
|
||||
def render("relationships.json", %{user: user, targets: targets}) do
|
||||
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
|
||||
end
|
||||
|
||||
defp username_from_nickname(string) when is_binary(string) do
|
||||
hd(String.split(string, "@"))
|
||||
end
|
||||
|
||||
defp username_from_nickname(_), do: nil
|
||||
|
||||
defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
Map.put(data, :notification_settings, user.info.notification_settings)
|
||||
end
|
||||
|
||||
defp with_notification_settings(data, _, _), do: data
|
||||
end
|
||||
|
|
|
|||
41
lib/pleroma/web/mastodon_api/views/app_view.ex
Normal file
41
lib/pleroma/web/mastodon_api/views/app_view.ex
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.AppView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Web.OAuth.App
|
||||
|
||||
@vapid_key :web_push_encryption
|
||||
|> Application.get_env(:vapid_details, [])
|
||||
|> Keyword.get(:public_key)
|
||||
|
||||
def render("show.json", %{app: %App{} = app}) do
|
||||
%{
|
||||
id: app.id |> to_string,
|
||||
name: app.client_name,
|
||||
client_id: app.client_id,
|
||||
client_secret: app.client_secret,
|
||||
redirect_uri: app.redirect_uris,
|
||||
website: app.website
|
||||
}
|
||||
|> with_vapid_key()
|
||||
end
|
||||
|
||||
def render("short.json", %{app: %App{website: webiste, client_name: name}}) do
|
||||
%{
|
||||
name: name,
|
||||
website: webiste
|
||||
}
|
||||
|> with_vapid_key()
|
||||
end
|
||||
|
||||
defp with_vapid_key(data) do
|
||||
if @vapid_key do
|
||||
Map.put(data, "vapid_key", @vapid_key)
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.FilterView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.MastodonAPI.FilterView
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.FilterView
|
||||
|
||||
def render("filters.json", %{filters: filters} = opts) do
|
||||
render_many(filters, FilterView, "filter.json", opts)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.ListView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.MastodonAPI.ListView
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.MastodonView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
import Phoenix.HTML.Form
|
||||
end
|
||||
|
|
|
|||
64
lib/pleroma/web/mastodon_api/views/notification_view.ex
Normal file
64
lib/pleroma/web/mastodon_api/views/notification_view.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("index.json", %{notifications: notifications, for: user}) do
|
||||
render_many(notifications, NotificationView, "show.json", %{for: user})
|
||||
end
|
||||
|
||||
def render("show.json", %{
|
||||
notification: %Notification{activity: activity} = notification,
|
||||
for: user
|
||||
}) do
|
||||
actor = User.get_cached_by_ap_id(activity.data["actor"])
|
||||
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
|
||||
mastodon_type = Activity.mastodon_notification_type(activity)
|
||||
|
||||
response = %{
|
||||
id: to_string(notification.id),
|
||||
type: mastodon_type,
|
||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||
account: AccountView.render("account.json", %{user: actor, for: user}),
|
||||
pleroma: %{
|
||||
is_seen: notification.seen
|
||||
}
|
||||
}
|
||||
|
||||
case mastodon_type do
|
||||
"mention" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: activity, for: user})
|
||||
})
|
||||
|
||||
"favourite" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"reblog" ->
|
||||
response
|
||||
|> Map.merge(%{
|
||||
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
|
||||
})
|
||||
|
||||
"follow" ->
|
||||
response
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
19
lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
Normal file
19
lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.Push
|
||||
|
||||
def render("push_subscription.json", %{subscription: subscription}) do
|
||||
%{
|
||||
id: to_string(subscription.id),
|
||||
endpoint: subscription.endpoint,
|
||||
alerts: Map.get(subscription.data, "alerts"),
|
||||
server_key: server_key()
|
||||
}
|
||||
end
|
||||
|
||||
defp server_key, do: Keyword.get(Push.vapid_config(), :public_key)
|
||||
end
|
||||
14
lib/pleroma/web/mastodon_api/views/report_view.ex
Normal file
14
lib/pleroma/web/mastodon_api/views/report_view.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.ReportView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
def render("report.json", %{activity: activity}) do
|
||||
%{
|
||||
id: to_string(activity.id),
|
||||
action_taken: false
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.ScheduledActivity
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("index.json", %{scheduled_activities: scheduled_activities}) do
|
||||
render_many(scheduled_activities, ScheduledActivityView, "show.json")
|
||||
end
|
||||
|
||||
def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
|
||||
%{
|
||||
id: to_string(scheduled_activity.id),
|
||||
scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at),
|
||||
params: status_params(scheduled_activity.params)
|
||||
}
|
||||
|> with_media_attachments(scheduled_activity)
|
||||
end
|
||||
|
||||
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
|
||||
try do
|
||||
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
|
||||
Map.put(data, :media_attachments, attachments)
|
||||
rescue
|
||||
_ -> data
|
||||
end
|
||||
end
|
||||
|
||||
defp with_media_attachments(data, _), do: data
|
||||
|
||||
defp status_params(params) do
|
||||
data = %{
|
||||
text: params["status"],
|
||||
sensitive: params["sensitive"],
|
||||
spoiler_text: params["spoiler_text"],
|
||||
visibility: params["visibility"],
|
||||
scheduled_at: params["scheduled_at"],
|
||||
poll: params["poll"],
|
||||
in_reply_to_id: params["in_reply_to_id"]
|
||||
}
|
||||
|
||||
data =
|
||||
if media_ids = params["media_ids"] do
|
||||
Map.put(data, :media_ids, media_ids)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.StatusView do
|
||||
use Pleroma.Web, :view
|
||||
alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
|
||||
alias Pleroma.{User, Activity, Object}
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Repo
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
# TODO: Add cached version.
|
||||
defp get_replied_to_activities(activities) do
|
||||
|
|
@ -19,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
nil
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Activity.create_activity_by_object_id_query()
|
||||
|> Activity.create_by_object_ap_id()
|
||||
|> Repo.all()
|
||||
|> Enum.reduce(%{}, fn activity, acc ->
|
||||
object = Object.normalize(activity.data["object"])
|
||||
|
|
@ -27,27 +36,52 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end)
|
||||
end
|
||||
|
||||
defp get_user(ap_id) do
|
||||
cond do
|
||||
user = User.get_cached_by_ap_id(ap_id) ->
|
||||
user
|
||||
|
||||
user = User.get_by_guessed_nickname(ap_id) ->
|
||||
user
|
||||
|
||||
true ->
|
||||
User.error_user(ap_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
|
||||
do: context_id
|
||||
|
||||
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
|
||||
do: Utils.context_to_conversation_id(context)
|
||||
|
||||
defp get_context_id(_), do: nil
|
||||
|
||||
defp reblogged?(activity, user) do
|
||||
object = Object.normalize(activity) || %{}
|
||||
present?(user && user.ap_id in (object.data["announcements"] || []))
|
||||
end
|
||||
|
||||
def render("index.json", opts) do
|
||||
replied_to_activities = get_replied_to_activities(opts.activities)
|
||||
|
||||
render_many(
|
||||
opts.activities,
|
||||
opts.activities
|
||||
|> safe_render_many(
|
||||
StatusView,
|
||||
"status.json",
|
||||
Map.put(opts, :replied_to_activities, replied_to_activities)
|
||||
)
|
||||
|> Enum.filter(fn x -> not is_nil(x) end)
|
||||
end
|
||||
|
||||
def render(
|
||||
"status.json",
|
||||
%{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts
|
||||
) do
|
||||
user = User.get_cached_by_ap_id(activity.data["actor"])
|
||||
user = get_user(activity.data["actor"])
|
||||
created_at = Utils.to_masto_date(activity.data["published"])
|
||||
|
||||
reblogged = Activity.get_create_activity_by_object_ap_id(object)
|
||||
reblogged = render("status.json", Map.put(opts, :activity, reblogged))
|
||||
reblogged_activity = Activity.get_create_by_object_ap_id(object)
|
||||
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
|
||||
|
||||
mentions =
|
||||
activity.recipients
|
||||
|
|
@ -68,28 +102,33 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
favourites_count: 0,
|
||||
reblogged: false,
|
||||
reblogged: reblogged?(reblogged_activity, opts[:for]),
|
||||
favourited: false,
|
||||
bookmarked: false,
|
||||
muted: false,
|
||||
pinned: pinned?(activity, user),
|
||||
sensitive: false,
|
||||
spoiler_text: "",
|
||||
visibility: "public",
|
||||
media_attachments: [],
|
||||
media_attachments: reblogged[:media_attachments] || [],
|
||||
mentions: mentions,
|
||||
tags: [],
|
||||
tags: reblogged[:tags] || [],
|
||||
application: %{
|
||||
name: "Web",
|
||||
website: nil
|
||||
},
|
||||
language: nil,
|
||||
emojis: []
|
||||
emojis: [],
|
||||
pleroma: %{
|
||||
local: activity.local
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do
|
||||
object = Object.normalize(object)
|
||||
|
||||
user = User.get_cached_by_ap_id(activity.data["actor"])
|
||||
user = get_user(activity.data["actor"])
|
||||
|
||||
like_count = object.data["like_count"] || 0
|
||||
announcement_count = object.data["announcement_count"] || 0
|
||||
|
|
@ -103,63 +142,101 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|> Enum.filter(& &1)
|
||||
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
|
||||
|
||||
repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || [])
|
||||
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
|
||||
|
||||
bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks
|
||||
|
||||
attachment_data = object.data["attachment"] || []
|
||||
attachment_data = attachment_data ++ if object.data["type"] == "Video", do: [object], else: []
|
||||
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
|
||||
|
||||
created_at = Utils.to_masto_date(object.data["published"])
|
||||
|
||||
reply_to = get_reply_to(activity, opts)
|
||||
reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
|
||||
|
||||
emojis =
|
||||
(object.data["emoji"] || [])
|
||||
|> Enum.map(fn {name, url} ->
|
||||
name = HTML.strip_tags(name)
|
||||
|
||||
url =
|
||||
HTML.strip_tags(url)
|
||||
|> MediaProxy.url()
|
||||
|
||||
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
|
||||
end)
|
||||
reply_to_user = reply_to && get_user(reply_to.data["actor"])
|
||||
|
||||
content =
|
||||
render_content(object.data)
|
||||
|> HTML.filter_tags(User.html_filter_policy(opts[:for]))
|
||||
object
|
||||
|> render_content()
|
||||
|
||||
content_html =
|
||||
content
|
||||
|> HTML.get_cached_scrubbed_html_for_activity(
|
||||
User.html_filter_policy(opts[:for]),
|
||||
activity,
|
||||
"mastoapi:content"
|
||||
)
|
||||
|
||||
content_plaintext =
|
||||
content
|
||||
|> HTML.get_cached_stripped_html_for_activity(
|
||||
activity,
|
||||
"mastoapi:content"
|
||||
)
|
||||
|
||||
summary = object.data["summary"] || ""
|
||||
|
||||
summary_html =
|
||||
summary
|
||||
|> HTML.get_cached_scrubbed_html_for_activity(
|
||||
User.html_filter_policy(opts[:for]),
|
||||
activity,
|
||||
"mastoapi:summary"
|
||||
)
|
||||
|
||||
summary_plaintext =
|
||||
summary
|
||||
|> HTML.get_cached_stripped_html_for_activity(
|
||||
activity,
|
||||
"mastoapi:summary"
|
||||
)
|
||||
|
||||
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
|
||||
|
||||
url =
|
||||
if user.local do
|
||||
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
|
||||
else
|
||||
object.data["external_url"] || object.data["id"]
|
||||
end
|
||||
|
||||
%{
|
||||
id: to_string(activity.id),
|
||||
uri: object.data["id"],
|
||||
url: object.data["external_url"] || object.data["id"],
|
||||
url: url,
|
||||
account: AccountView.render("account.json", %{user: user}),
|
||||
in_reply_to_id: reply_to && to_string(reply_to.id),
|
||||
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
|
||||
reblog: nil,
|
||||
content: content,
|
||||
card: card,
|
||||
content: content_html,
|
||||
created_at: created_at,
|
||||
reblogs_count: announcement_count,
|
||||
replies_count: 0,
|
||||
replies_count: object.data["repliesCount"] || 0,
|
||||
favourites_count: like_count,
|
||||
reblogged: !!repeated,
|
||||
favourited: !!favorited,
|
||||
muted: false,
|
||||
reblogged: reblogged?(activity, opts[:for]),
|
||||
favourited: present?(favorited),
|
||||
bookmarked: present?(bookmarked),
|
||||
muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
|
||||
pinned: pinned?(activity, user),
|
||||
sensitive: sensitive,
|
||||
spoiler_text: object.data["summary"] || "",
|
||||
visibility: get_visibility(object.data),
|
||||
media_attachments: attachments |> Enum.take(4),
|
||||
spoiler_text: summary_html,
|
||||
visibility: get_visibility(object),
|
||||
media_attachments: attachments,
|
||||
mentions: mentions,
|
||||
# fix,
|
||||
tags: [],
|
||||
tags: build_tags(tags),
|
||||
application: %{
|
||||
name: "Web",
|
||||
website: nil
|
||||
},
|
||||
language: nil,
|
||||
emojis: emojis
|
||||
emojis: build_emojis(object.data["emoji"]),
|
||||
pleroma: %{
|
||||
local: activity.local,
|
||||
conversation_id: get_context_id(activity),
|
||||
content: %{"text/plain" => content_plaintext},
|
||||
spoiler_text: %{"text/plain" => summary_plaintext}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -167,6 +244,46 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
nil
|
||||
end
|
||||
|
||||
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
|
||||
page_url_data = URI.parse(page_url)
|
||||
|
||||
page_url_data =
|
||||
if rich_media[:url] != nil do
|
||||
URI.merge(page_url_data, URI.parse(rich_media[:url]))
|
||||
else
|
||||
page_url_data
|
||||
end
|
||||
|
||||
page_url = page_url_data |> to_string
|
||||
|
||||
image_url =
|
||||
if rich_media[:image] != nil do
|
||||
URI.merge(page_url_data, URI.parse(rich_media[:image]))
|
||||
|> to_string
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
site_name = rich_media[:site_name] || page_url_data.host
|
||||
|
||||
%{
|
||||
type: "link",
|
||||
provider_name: site_name,
|
||||
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
|
||||
url: page_url,
|
||||
image: image_url |> MediaProxy.url(),
|
||||
title: rich_media[:title],
|
||||
description: rich_media[:description],
|
||||
pleroma: %{
|
||||
opengraph: rich_media
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def render("card.json", _) do
|
||||
nil
|
||||
end
|
||||
|
||||
def render("attachment.json", %{attachment: attachment}) do
|
||||
[attachment_url | _] = attachment["url"]
|
||||
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
|
||||
|
|
@ -189,20 +306,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
preview_url: href,
|
||||
text_url: href,
|
||||
type: type,
|
||||
description: attachment["name"]
|
||||
description: attachment["name"],
|
||||
pleroma: %{mime_type: media_type}
|
||||
}
|
||||
end
|
||||
|
||||
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
|
||||
object = Object.normalize(activity.data["object"])
|
||||
replied_to_activities[object.data["inReplyTo"]]
|
||||
|
||||
with nil <- replied_to_activities[object.data["inReplyTo"]] do
|
||||
# If user didn't participate in the thread
|
||||
Activity.get_in_reply_to_activity(activity)
|
||||
end
|
||||
end
|
||||
|
||||
def get_reply_to(%{data: %{"object" => object}}, _) do
|
||||
object = Object.normalize(object)
|
||||
|
||||
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
|
||||
Activity.get_create_activity_by_object_ap_id(object.data["inReplyTo"])
|
||||
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
|
@ -210,8 +332,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
def get_visibility(object) do
|
||||
public = "https://www.w3.org/ns/activitystreams#Public"
|
||||
to = object["to"] || []
|
||||
cc = object["cc"] || []
|
||||
to = object.data["to"] || []
|
||||
cc = object.data["cc"] || []
|
||||
|
||||
cond do
|
||||
public in to ->
|
||||
|
|
@ -224,36 +346,89 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
Enum.any?(to, &String.contains?(&1, "/followers")) ->
|
||||
"private"
|
||||
|
||||
length(cc) > 0 ->
|
||||
"private"
|
||||
|
||||
true ->
|
||||
"direct"
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(%{"type" => "Video"} = object) do
|
||||
name = object["name"]
|
||||
|
||||
content =
|
||||
if !!name and name != "" do
|
||||
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
|
||||
else
|
||||
object["content"] || ""
|
||||
end
|
||||
|
||||
content
|
||||
def render_content(%{data: %{"type" => "Video"}} = object) do
|
||||
with name when not is_nil(name) and name != "" <- object.data["name"] do
|
||||
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
|
||||
else
|
||||
_ -> object.data["content"] || ""
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(%{"type" => object_type} = object) when object_type in ["Article", "Page"] do
|
||||
summary = object["name"]
|
||||
|
||||
content =
|
||||
if !!summary and summary != "" and is_bitstring(object["url"]) do
|
||||
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
|
||||
else
|
||||
object["content"] || ""
|
||||
end
|
||||
|
||||
content
|
||||
def render_content(%{data: %{"type" => object_type}} = object)
|
||||
when object_type in ["Article", "Page"] do
|
||||
with summary when not is_nil(summary) and summary != "" <- object.data["name"],
|
||||
url when is_bitstring(url) <- object.data["url"] do
|
||||
"<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
|
||||
else
|
||||
_ -> object.data["content"] || ""
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(object), do: object["content"] || ""
|
||||
def render_content(object), do: object.data["content"] || ""
|
||||
|
||||
@doc """
|
||||
Builds a dictionary tags.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
|
||||
[{"name": "fediverse", "url": "/tag/fediverse"},
|
||||
{"name": "nextcloud", "url": "/tag/nextcloud"}]
|
||||
|
||||
"""
|
||||
@spec build_tags(list(any())) :: list(map())
|
||||
def build_tags(object_tags) when is_list(object_tags) do
|
||||
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
|
||||
|
||||
Enum.reduce(object_tags, [], fn tag, tags ->
|
||||
tags ++ [%{name: tag, url: "/tag/#{tag}"}]
|
||||
end)
|
||||
end
|
||||
|
||||
def build_tags(_), do: []
|
||||
|
||||
@doc """
|
||||
Builds list emojis.
|
||||
|
||||
Arguments: `nil` or list tuple of name and url.
|
||||
|
||||
Returns list emojis.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
|
||||
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
|
||||
|
||||
"""
|
||||
@spec build_emojis(nil | list(tuple())) :: list(map())
|
||||
def build_emojis(nil), do: []
|
||||
|
||||
def build_emojis(emojis) do
|
||||
emojis
|
||||
|> Enum.map(fn {name, url} ->
|
||||
name = HTML.strip_tags(name)
|
||||
|
||||
url =
|
||||
url
|
||||
|> HTML.strip_tags()
|
||||
|> MediaProxy.url()
|
||||
|
||||
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
|
||||
end)
|
||||
end
|
||||
|
||||
defp present?(nil), do: false
|
||||
defp present?(false), do: false
|
||||
defp present?(_), do: true
|
||||
|
||||
defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
|
||||
do: id in pinned_activities
|
||||
end
|
||||
|
|
|
|||
125
lib/pleroma/web/mastodon_api/websocket_handler.ex
Normal file
125
lib/pleroma/web/mastodon_api/websocket_handler.ex
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
@behaviour :cowboy_websocket
|
||||
|
||||
@streams [
|
||||
"public",
|
||||
"public:local",
|
||||
"public:media",
|
||||
"public:local:media",
|
||||
"user",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
]
|
||||
@anonymous_streams ["public", "public:local", "hashtag"]
|
||||
|
||||
# Handled by periodic keepalive in Pleroma.Web.Streamer.
|
||||
@timeout :infinity
|
||||
|
||||
def init(%{qs: qs} = req, state) do
|
||||
with params <- :cow_qs.parse_qs(qs),
|
||||
access_token <- List.keyfind(params, "access_token", 0),
|
||||
{_, stream} <- List.keyfind(params, "stream", 0),
|
||||
{:ok, user} <- allow_request(stream, access_token),
|
||||
topic when is_binary(topic) <- expand_topic(stream, params) do
|
||||
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
|
||||
else
|
||||
{:error, code} ->
|
||||
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
|
||||
{:ok, req} = :cowboy_req.reply(code, req)
|
||||
{:ok, req, state}
|
||||
|
||||
error ->
|
||||
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
|
||||
{:ok, req} = :cowboy_req.reply(400, req)
|
||||
{:ok, req, state}
|
||||
end
|
||||
end
|
||||
|
||||
def websocket_init(state) do
|
||||
send(self(), :subscribe)
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
# We never receive messages.
|
||||
def websocket_handle(_frame, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def websocket_info(:subscribe, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} accepted websocket connection for user #{
|
||||
(state.user || %{id: "anonymous"}).id
|
||||
}, topic #{state.topic}"
|
||||
)
|
||||
|
||||
Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def websocket_info({:text, message}, state) do
|
||||
{:reply, {:text, message}, state}
|
||||
end
|
||||
|
||||
def terminate(reason, _req, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} terminating websocket connection for user #{
|
||||
(state.user || %{id: "anonymous"}).id
|
||||
}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
Pleroma.Web.Streamer.remove_socket(state.topic, streamer_socket(state))
|
||||
:ok
|
||||
end
|
||||
|
||||
# Public streams without authentication.
|
||||
defp allow_request(stream, nil) when stream in @anonymous_streams do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
# Authenticated streams.
|
||||
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
|
||||
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
|
||||
user = %User{} <- User.get_by_id(user_id) do
|
||||
{:ok, user}
|
||||
else
|
||||
_ -> {:error, 403}
|
||||
end
|
||||
end
|
||||
|
||||
# Not authenticated.
|
||||
defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
|
||||
|
||||
# No matching stream.
|
||||
defp allow_request(_, _), do: {:error, 404}
|
||||
|
||||
defp expand_topic("hashtag", params) do
|
||||
case List.keyfind(params, "tag", 0) do
|
||||
{_, tag} -> "hashtag:#{tag}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_topic("list", params) do
|
||||
case List.keyfind(params, "list", 0) do
|
||||
{_, list} -> "list:#{list}"
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_topic(topic, _), do: topic
|
||||
|
||||
defp streamer_socket(state) do
|
||||
%{transport_pid: self(), assigns: state}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{Web.MediaProxy, ReverseProxy}
|
||||
alias Pleroma.ReverseProxy
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576]
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
||||
|
||||
def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
|
||||
with config <- Pleroma.Config.get([:media_proxy], []),
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
filename <- Path.basename(URI.parse(url).path),
|
||||
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
|
||||
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
|
||||
else
|
||||
|
|
@ -24,11 +28,17 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
|||
end
|
||||
|
||||
def filename_matches(has_filename, path, url) do
|
||||
filename = MediaProxy.filename(url)
|
||||
filename =
|
||||
url
|
||||
|> MediaProxy.filename()
|
||||
|> URI.decode()
|
||||
|
||||
cond do
|
||||
has_filename && filename && Path.basename(path) != filename -> {:wrong_filename, filename}
|
||||
true -> :ok
|
||||
path = URI.decode(path)
|
||||
|
||||
if has_filename && filename && Path.basename(path) != filename do
|
||||
{:wrong_filename, filename}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MediaProxy do
|
||||
@base64_opts [padding: false]
|
||||
|
||||
|
|
@ -5,7 +9,7 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
|
||||
def url(""), do: nil
|
||||
|
||||
def url(url = "/" <> _), do: url
|
||||
def url("/" <> _ = url), do: url
|
||||
|
||||
def url(url) do
|
||||
config = Application.get_env(:pleroma, :media_proxy, [])
|
||||
|
|
@ -14,7 +18,20 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
url
|
||||
else
|
||||
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
|
||||
base64 = Base.url_encode64(url, @base64_opts)
|
||||
|
||||
# Must preserve `%2F` for compatibility with S3
|
||||
# https://git.pleroma.social/pleroma/pleroma/issues/580
|
||||
replacement = get_replacement(url, ":2F:")
|
||||
|
||||
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
|
||||
base64 =
|
||||
url
|
||||
|> String.replace("%2F", replacement)
|
||||
|> URI.decode()
|
||||
|> URI.encode()
|
||||
|> String.replace(replacement, "%2F")
|
||||
|> Base.url_encode64(@base64_opts)
|
||||
|
||||
sig = :crypto.hmac(:sha, secret, base64)
|
||||
sig64 = sig |> Base.url_encode64(@base64_opts)
|
||||
|
||||
|
|
@ -49,4 +66,12 @@ defmodule Pleroma.Web.MediaProxy do
|
|||
|> Enum.filter(fn value -> value end)
|
||||
|> Path.join()
|
||||
end
|
||||
|
||||
defp get_replacement(url, replacement) do
|
||||
if String.contains?(url, replacement) do
|
||||
get_replacement(url, replacement <> replacement)
|
||||
else
|
||||
replacement
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
40
lib/pleroma/web/metadata.ex
Normal file
40
lib/pleroma/web/metadata.ex
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Metadata do
|
||||
alias Phoenix.HTML
|
||||
|
||||
def build_tags(params) do
|
||||
Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), "", fn parser, acc ->
|
||||
rendered_html =
|
||||
params
|
||||
|> parser.build_tags()
|
||||
|> Enum.map(&to_tag/1)
|
||||
|> Enum.map(&HTML.safe_to_string/1)
|
||||
|> Enum.join()
|
||||
|
||||
acc <> rendered_html
|
||||
end)
|
||||
end
|
||||
|
||||
def to_tag(data) do
|
||||
with {name, attrs, _content = []} <- data do
|
||||
HTML.Tag.tag(name, attrs)
|
||||
else
|
||||
{name, attrs, content} ->
|
||||
HTML.Tag.content_tag(name, content, attrs)
|
||||
|
||||
_ ->
|
||||
raise ArgumentError, message: "make_tag invalid args"
|
||||
end
|
||||
end
|
||||
|
||||
def activity_nsfw?(%{data: %{"sensitive" => sensitive}}) do
|
||||
Pleroma.Config.get([__MODULE__, :unfurl_nsfw], false) == false and sensitive
|
||||
end
|
||||
|
||||
def activity_nsfw?(_) do
|
||||
false
|
||||
end
|
||||
end
|
||||
124
lib/pleroma/web/metadata/opengraph.ex
Normal file
124
lib/pleroma/web/metadata/opengraph.ex
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Metadata
|
||||
alias Pleroma.Web.Metadata.Providers.Provider
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@impl Provider
|
||||
def build_tags(%{
|
||||
object: object,
|
||||
url: url,
|
||||
user: user
|
||||
}) do
|
||||
attachments = build_attachments(object)
|
||||
scrubbed_content = Utils.scrub_html_and_truncate(object)
|
||||
# Zero width space
|
||||
content =
|
||||
if scrubbed_content != "" and scrubbed_content != "\u200B" do
|
||||
": “" <> scrubbed_content <> "”"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
# Most previews only show og:title which is inconvenient. Instagram
|
||||
# hacks this by putting the description in the title and making the
|
||||
# description longer prefixed by how many likes and shares the post
|
||||
# has. Here we use the descriptive nickname in the title, and expand
|
||||
# the full account & nickname in the description. We also use the cute^Wevil
|
||||
# smart quotes around the status text like Instagram, too.
|
||||
[
|
||||
{:meta,
|
||||
[
|
||||
property: "og:title",
|
||||
content: "#{user.name}" <> content
|
||||
], []},
|
||||
{:meta, [property: "og:url", content: url], []},
|
||||
{:meta,
|
||||
[
|
||||
property: "og:description",
|
||||
content: "#{Utils.user_name_string(user)}" <> content
|
||||
], []},
|
||||
{:meta, [property: "og:type", content: "website"], []}
|
||||
] ++
|
||||
if attachments == [] or Metadata.activity_nsfw?(object) do
|
||||
[
|
||||
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))],
|
||||
[]},
|
||||
{:meta, [property: "og:image:width", content: 150], []},
|
||||
{:meta, [property: "og:image:height", content: 150], []}
|
||||
]
|
||||
else
|
||||
attachments
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def build_tags(%{user: user}) do
|
||||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
|
||||
[
|
||||
{:meta,
|
||||
[
|
||||
property: "og:title",
|
||||
content: Utils.user_name_string(user)
|
||||
], []},
|
||||
{:meta, [property: "og:url", content: User.profile_url(user)], []},
|
||||
{:meta, [property: "og:description", content: truncated_bio], []},
|
||||
{:meta, [property: "og:type", content: "website"], []},
|
||||
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []},
|
||||
{:meta, [property: "og:image:width", content: 150], []},
|
||||
{:meta, [property: "og:image:height", content: 150], []}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_attachments(%{data: %{"attachment" => attachments}}) do
|
||||
Enum.reduce(attachments, [], fn attachment, acc ->
|
||||
rendered_tags =
|
||||
Enum.reduce(attachment["url"], [], fn url, acc ->
|
||||
media_type =
|
||||
Enum.find(["image", "audio", "video"], fn media_type ->
|
||||
String.starts_with?(url["mediaType"], media_type)
|
||||
end)
|
||||
|
||||
# TODO: Add additional properties to objects when we have the data available.
|
||||
# Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
|
||||
# object when a Video or GIF is attached it will display that in Whatsapp Rich Preview.
|
||||
case media_type do
|
||||
"audio" ->
|
||||
[
|
||||
{:meta,
|
||||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
"image" ->
|
||||
[
|
||||
{:meta,
|
||||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []},
|
||||
{:meta, [property: "og:image:width", content: 150], []},
|
||||
{:meta, [property: "og:image:height", content: 150], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
"video" ->
|
||||
[
|
||||
{:meta,
|
||||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
acc ++ rendered_tags
|
||||
end)
|
||||
end
|
||||
end
|
||||
21
lib/pleroma/web/metadata/player_view.ex
Normal file
21
lib/pleroma/web/metadata/player_view.ex
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Pleroma.Web.Metadata.PlayerView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2]
|
||||
|
||||
def render("player.html", %{"mediaType" => type, "href" => href}) do
|
||||
{tag_type, tag_attrs} =
|
||||
case type do
|
||||
"audio" <> _ -> {:audio, []}
|
||||
"video" <> _ -> {:video, [loop: true]}
|
||||
end
|
||||
|
||||
content_tag(
|
||||
tag_type,
|
||||
[
|
||||
tag(:source, src: href, type: type),
|
||||
"Your browser does not support #{type} playback."
|
||||
],
|
||||
[controls: true] ++ tag_attrs
|
||||
)
|
||||
end
|
||||
end
|
||||
7
lib/pleroma/web/metadata/provider.ex
Normal file
7
lib/pleroma/web/metadata/provider.ex
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Metadata.Providers.Provider do
|
||||
@callback build_tags(map()) :: list()
|
||||
end
|
||||
123
lib/pleroma/web/metadata/twitter_card.ex
Normal file
123
lib/pleroma/web/metadata/twitter_card.ex
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Metadata
|
||||
alias Pleroma.Web.Metadata.Providers.Provider
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
|
||||
@behaviour Provider
|
||||
|
||||
@impl Provider
|
||||
def build_tags(%{
|
||||
activity_id: id,
|
||||
object: object,
|
||||
user: user
|
||||
}) do
|
||||
attachments = build_attachments(id, object)
|
||||
scrubbed_content = Utils.scrub_html_and_truncate(object)
|
||||
# Zero width space
|
||||
content =
|
||||
if scrubbed_content != "" and scrubbed_content != "\u200B" do
|
||||
"“" <> scrubbed_content <> "”"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
[
|
||||
{:meta,
|
||||
[
|
||||
property: "twitter:title",
|
||||
content: Utils.user_name_string(user)
|
||||
], []},
|
||||
{:meta,
|
||||
[
|
||||
property: "twitter:description",
|
||||
content: content
|
||||
], []}
|
||||
] ++
|
||||
if attachments == [] or Metadata.activity_nsfw?(object) do
|
||||
[
|
||||
{:meta,
|
||||
[property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []},
|
||||
{:meta, [property: "twitter:card", content: "summary_large_image"], []}
|
||||
]
|
||||
else
|
||||
attachments
|
||||
end
|
||||
end
|
||||
|
||||
@impl Provider
|
||||
def build_tags(%{user: user}) do
|
||||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
|
||||
[
|
||||
{:meta,
|
||||
[
|
||||
property: "twitter:title",
|
||||
content: Utils.user_name_string(user)
|
||||
], []},
|
||||
{:meta, [property: "twitter:description", content: truncated_bio], []},
|
||||
{:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))],
|
||||
[]},
|
||||
{:meta, [property: "twitter:card", content: "summary"], []}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
|
||||
Enum.reduce(attachments, [], fn attachment, acc ->
|
||||
rendered_tags =
|
||||
Enum.reduce(attachment["url"], [], fn url, acc ->
|
||||
media_type =
|
||||
Enum.find(["image", "audio", "video"], fn media_type ->
|
||||
String.starts_with?(url["mediaType"], media_type)
|
||||
end)
|
||||
|
||||
# TODO: Add additional properties to objects when we have the data available.
|
||||
case media_type do
|
||||
"audio" ->
|
||||
[
|
||||
{:meta, [property: "twitter:card", content: "player"], []},
|
||||
{:meta, [property: "twitter:player:width", content: "480"], []},
|
||||
{:meta, [property: "twitter:player:height", content: "80"], []},
|
||||
{:meta, [property: "twitter:player", content: player_url(id)], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
"image" ->
|
||||
[
|
||||
{:meta, [property: "twitter:card", content: "summary_large_image"], []},
|
||||
{:meta,
|
||||
[
|
||||
property: "twitter:player",
|
||||
content: Utils.attachment_url(url["href"])
|
||||
], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
# TODO: Need the true width and height values here or Twitter renders an iFrame with
|
||||
# a bad aspect ratio
|
||||
"video" ->
|
||||
[
|
||||
{:meta, [property: "twitter:card", content: "player"], []},
|
||||
{:meta, [property: "twitter:player", content: player_url(id)], []},
|
||||
{:meta, [property: "twitter:player:width", content: "480"], []},
|
||||
{:meta, [property: "twitter:player:height", content: "480"], []}
|
||||
| acc
|
||||
]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
acc ++ rendered_tags
|
||||
end)
|
||||
end
|
||||
|
||||
defp player_url(id) do
|
||||
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id)
|
||||
end
|
||||
end
|
||||
42
lib/pleroma/web/metadata/utils.ex
Normal file
42
lib/pleroma/web/metadata/utils.ex
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Metadata.Utils do
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
|
||||
content
|
||||
# html content comes from DB already encoded, decode first and scrub after
|
||||
|> HtmlEntities.decode()
|
||||
|> String.replace(~r/<br\s?\/?>/, " ")
|
||||
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|
||||
|> Formatter.demojify()
|
||||
|> Formatter.truncate()
|
||||
end
|
||||
|
||||
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
|
||||
content
|
||||
# html content comes from DB already encoded, decode first and scrub after
|
||||
|> HtmlEntities.decode()
|
||||
|> String.replace(~r/<br\s?\/?>/, " ")
|
||||
|> HTML.strip_tags()
|
||||
|> Formatter.demojify()
|
||||
|> Formatter.truncate(max_length)
|
||||
end
|
||||
|
||||
def attachment_url(url) do
|
||||
MediaProxy.url(url)
|
||||
end
|
||||
|
||||
def user_name_string(user) do
|
||||
"#{user.name} " <>
|
||||
if user.local do
|
||||
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
|
||||
else
|
||||
"(@#{user.nickname})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Stats
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Stats
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug)
|
||||
|
|
@ -15,6 +19,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
%{
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
href: Web.base_url() <> "/nodeinfo/2.0.json"
|
||||
},
|
||||
%{
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
||||
href: Web.base_url() <> "/nodeinfo/2.1.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -22,8 +30,9 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
json(conn, response)
|
||||
end
|
||||
|
||||
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
|
||||
def nodeinfo(conn, %{"version" => "2.0"}) do
|
||||
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
|
||||
# under software.
|
||||
def raw_nodeinfo do
|
||||
instance = Application.get_env(:pleroma, :instance)
|
||||
media_proxy = Application.get_env(:pleroma, :media_proxy)
|
||||
suggestions = Application.get_env(:pleroma, :suggestions)
|
||||
|
|
@ -35,6 +44,33 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
Application.get_env(:pleroma, :mrf_simple)
|
||||
|> Enum.into(%{})
|
||||
|
||||
# This horror is needed to convert regex sigils to strings
|
||||
mrf_keyword =
|
||||
Application.get_env(:pleroma, :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)
|
||||
|
|
@ -49,21 +85,19 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
end
|
||||
|
||||
staff_accounts =
|
||||
User.moderator_user_query()
|
||||
|> Repo.all()
|
||||
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)
|
||||
|
||||
mrf_transparency = Keyword.get(instance, :mrf_transparency)
|
||||
|
||||
federation_response =
|
||||
if mrf_transparency do
|
||||
if Keyword.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
|
||||
}
|
||||
|
|
@ -71,28 +105,36 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
%{}
|
||||
end
|
||||
|
||||
features = [
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_streaming",
|
||||
if Keyword.get(media_proxy, :enabled) do
|
||||
"media_proxy"
|
||||
end,
|
||||
if Keyword.get(gopher, :enabled) do
|
||||
"gopher"
|
||||
end,
|
||||
if Keyword.get(chat, :enabled) do
|
||||
"chat"
|
||||
end,
|
||||
if Keyword.get(suggestions, :enabled) do
|
||||
"suggestions"
|
||||
end
|
||||
]
|
||||
features =
|
||||
[
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_streaming",
|
||||
if Keyword.get(media_proxy, :enabled) do
|
||||
"media_proxy"
|
||||
end,
|
||||
if Keyword.get(gopher, :enabled) do
|
||||
"gopher"
|
||||
end,
|
||||
if Keyword.get(chat, :enabled) do
|
||||
"chat"
|
||||
end,
|
||||
if Keyword.get(suggestions, :enabled) do
|
||||
"suggestions"
|
||||
end,
|
||||
if Keyword.get(instance, :allow_relay) do
|
||||
"relay"
|
||||
end,
|
||||
if Keyword.get(instance, :safe_dm_mentions) do
|
||||
"safe_dm_mentions"
|
||||
end
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
response = %{
|
||||
%{
|
||||
version: "2.0",
|
||||
software: %{
|
||||
name: Pleroma.Application.name(),
|
||||
name: Pleroma.Application.name() |> String.downcase(),
|
||||
version: Pleroma.Application.version()
|
||||
},
|
||||
protocols: ["ostatus", "activitypub"],
|
||||
|
|
@ -127,15 +169,43 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
|||
banner: Keyword.get(instance, :banner_upload_limit),
|
||||
background: Keyword.get(instance, :background_upload_limit)
|
||||
},
|
||||
features: features
|
||||
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
|
||||
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
|
||||
features: features,
|
||||
restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
|
||||
# and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
|
||||
def nodeinfo(conn, %{"version" => "2.0"}) do
|
||||
conn
|
||||
|> put_resp_header(
|
||||
"content-type",
|
||||
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
|
||||
)
|
||||
|> json(raw_nodeinfo())
|
||||
end
|
||||
|
||||
def nodeinfo(conn, %{"version" => "2.1"}) do
|
||||
raw_response = raw_nodeinfo()
|
||||
|
||||
updated_software =
|
||||
raw_response
|
||||
|> Map.get(:software)
|
||||
|> Map.put(:repository, Pleroma.Application.repository())
|
||||
|
||||
response =
|
||||
raw_response
|
||||
|> Map.put(:software, updated_software)
|
||||
|> Map.put(:version, "2.1")
|
||||
|
||||
conn
|
||||
|> put_resp_header(
|
||||
"content-type",
|
||||
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
|
||||
)
|
||||
|> json(response)
|
||||
end
|
||||
|
||||
|
|
|
|||
20
lib/pleroma/web/oauth.ex
Normal file
20
lib/pleroma/web/oauth.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth do
|
||||
def parse_scopes(scopes, _default) when is_list(scopes) do
|
||||
Enum.filter(scopes, &(&1 not in [nil, ""]))
|
||||
end
|
||||
|
||||
def parse_scopes(scopes, default) when is_binary(scopes) do
|
||||
scopes
|
||||
|> String.trim()
|
||||
|> String.split(~r/[\s,]+/)
|
||||
|> parse_scopes(default)
|
||||
end
|
||||
|
||||
def parse_scopes(_, default) do
|
||||
default
|
||||
end
|
||||
end
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.App do
|
||||
use Ecto.Schema
|
||||
import Ecto.{Changeset}
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "apps" do
|
||||
field(:client_name, :string)
|
||||
field(:redirect_uris, :string)
|
||||
field(:scopes, :string)
|
||||
field(:scopes, {:array, :string}, default: [])
|
||||
field(:website, :string)
|
||||
field(:client_id, :string)
|
||||
field(:client_secret, :string)
|
||||
|
|
@ -21,8 +25,14 @@ defmodule Pleroma.Web.OAuth.App do
|
|||
|
||||
if changeset.valid? do
|
||||
changeset
|
||||
|> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
|
||||
|> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
|
||||
|> put_change(
|
||||
:client_id,
|
||||
:crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||
)
|
||||
|> put_change(
|
||||
:client_secret,
|
||||
:crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||
)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,29 +1,39 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.Authorization do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.Web.OAuth.{Authorization, App}
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
|
||||
import Ecto.{Changeset, Query}
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
schema "oauth_authorizations" do
|
||||
field(:token, :string)
|
||||
field(:valid_until, :naive_datetime)
|
||||
field(:scopes, {:array, :string}, default: [])
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
field(:used, :boolean, default: false)
|
||||
belongs_to(:user, Pleroma.User)
|
||||
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
|
||||
belongs_to(:app, App)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def create_authorization(%App{} = app, %User{} = user) do
|
||||
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
|
||||
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
|
||||
scopes = scopes || app.scopes
|
||||
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||
|
||||
authorization = %Authorization{
|
||||
token: token,
|
||||
used: false,
|
||||
user_id: user.id,
|
||||
app_id: app.id,
|
||||
scopes: scopes,
|
||||
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.FallbackController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
|
||||
# No user/password
|
||||
def call(conn, _) do
|
||||
def call(conn, {:register, :generic_error}) do
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> put_flash(:error, "Unknown error, please check the details and try again.")
|
||||
|> OAuthController.registration_details(conn.params)
|
||||
end
|
||||
|
||||
def call(conn, {:register, _error}) do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> put_flash(:error, "Invalid Username/Password")
|
||||
|> OAuthController.authorize(conn.params)
|
||||
|> OAuthController.registration_details(conn.params)
|
||||
end
|
||||
|
||||
def call(conn, _error) do
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> put_flash(:error, "Invalid Username/Password")
|
||||
|> OAuthController.authorize(conn.params["authorization"])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,78 +1,132 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.OAuthController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Web.OAuth.{Authorization, Token, App}
|
||||
alias Pleroma.{Repo, User}
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Registration
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Auth.Authenticator
|
||||
alias Pleroma.Web.ControllerHelper
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
|
||||
|
||||
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
|
||||
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_flash)
|
||||
|
||||
action_fallback(Pleroma.Web.OAuth.FallbackController)
|
||||
|
||||
def authorize(conn, params) do
|
||||
render(conn, "show.html", %{
|
||||
response_type: params["response_type"],
|
||||
client_id: params["client_id"],
|
||||
scope: params["scope"],
|
||||
redirect_uri: params["redirect_uri"],
|
||||
state: params["state"]
|
||||
})
|
||||
end
|
||||
|
||||
def create_authorization(conn, %{
|
||||
"authorization" =>
|
||||
%{
|
||||
"name" => name,
|
||||
"password" => password,
|
||||
"client_id" => client_id,
|
||||
"redirect_uri" => redirect_uri
|
||||
} = params
|
||||
}) do
|
||||
with %User{} = user <- User.get_by_nickname_or_email(name),
|
||||
true <- Pbkdf2.checkpw(password, user.password_hash),
|
||||
%App{} = app <- Repo.get_by(App, client_id: client_id),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user) do
|
||||
# Special case: Local MastodonFE.
|
||||
def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
|
||||
if ControllerHelper.truthy_param?(params["force_login"]) do
|
||||
do_authorize(conn, params)
|
||||
else
|
||||
redirect_uri =
|
||||
if redirect_uri == "." do
|
||||
mastodon_api_url(conn, :login)
|
||||
if is_binary(params["redirect_uri"]) do
|
||||
params["redirect_uri"]
|
||||
else
|
||||
redirect_uri
|
||||
app = Repo.preload(token, :app).app
|
||||
|
||||
app.redirect_uris
|
||||
|> String.split()
|
||||
|> Enum.at(0)
|
||||
end
|
||||
|
||||
cond do
|
||||
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
|
||||
render(conn, "results.html", %{
|
||||
auth: auth
|
||||
})
|
||||
|
||||
true ->
|
||||
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
|
||||
url = "#{redirect_uri}#{connector}"
|
||||
url_params = %{:code => auth.token}
|
||||
|
||||
url_params =
|
||||
if params["state"] do
|
||||
Map.put(url_params, :state, params["state"])
|
||||
else
|
||||
url_params
|
||||
end
|
||||
|
||||
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
|
||||
|
||||
redirect(conn, external: url)
|
||||
end
|
||||
redirect(conn, external: redirect_uri(conn, redirect_uri))
|
||||
end
|
||||
end
|
||||
|
||||
# TODO
|
||||
# - proper scope handling
|
||||
def authorize(conn, params), do: do_authorize(conn, params)
|
||||
|
||||
defp do_authorize(conn, params) do
|
||||
app = Repo.get_by(App, client_id: params["client_id"])
|
||||
available_scopes = (app && app.scopes) || []
|
||||
scopes = oauth_scopes(params, nil) || available_scopes
|
||||
|
||||
render(conn, Authenticator.auth_template(), %{
|
||||
response_type: params["response_type"],
|
||||
client_id: params["client_id"],
|
||||
available_scopes: available_scopes,
|
||||
scopes: scopes,
|
||||
redirect_uri: params["redirect_uri"],
|
||||
state: params["state"],
|
||||
params: params
|
||||
})
|
||||
end
|
||||
|
||||
def create_authorization(
|
||||
conn,
|
||||
%{"authorization" => auth_params} = params,
|
||||
opts \\ []
|
||||
) do
|
||||
with {:ok, auth} <- do_create_authorization(conn, params, opts[:user]) do
|
||||
after_create_authorization(conn, auth, auth_params)
|
||||
else
|
||||
error ->
|
||||
handle_create_authorization_error(conn, error, auth_params)
|
||||
end
|
||||
end
|
||||
|
||||
def after_create_authorization(conn, auth, %{"redirect_uri" => redirect_uri} = auth_params) do
|
||||
redirect_uri = redirect_uri(conn, redirect_uri)
|
||||
|
||||
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
|
||||
render(conn, "results.html", %{
|
||||
auth: auth
|
||||
})
|
||||
else
|
||||
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
|
||||
url = "#{redirect_uri}#{connector}"
|
||||
url_params = %{:code => auth.token}
|
||||
|
||||
url_params =
|
||||
if auth_params["state"] do
|
||||
Map.put(url_params, :state, auth_params["state"])
|
||||
else
|
||||
url_params
|
||||
end
|
||||
|
||||
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
|
||||
|
||||
redirect(conn, external: url)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_create_authorization_error(conn, {scopes_issue, _}, auth_params)
|
||||
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
|
||||
# Per https://github.com/tootsuite/mastodon/blob/
|
||||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
|
||||
conn
|
||||
|> put_flash(:error, "This action is outside the authorized scopes")
|
||||
|> put_status(:unauthorized)
|
||||
|> authorize(auth_params)
|
||||
end
|
||||
|
||||
defp handle_create_authorization_error(conn, {:auth_active, false}, auth_params) do
|
||||
# Per https://github.com/tootsuite/mastodon/blob/
|
||||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
|
||||
conn
|
||||
|> put_flash(:error, "Your login is missing a confirmed e-mail address")
|
||||
|> put_status(:forbidden)
|
||||
|> authorize(auth_params)
|
||||
end
|
||||
|
||||
defp handle_create_authorization_error(conn, error, _auth_params) do
|
||||
Authenticator.handle_error(conn, error)
|
||||
end
|
||||
|
||||
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
|
||||
with %App{} = app <- get_app_from_request(conn, params),
|
||||
fixed_token = fix_padding(params["code"]),
|
||||
%Authorization{} = auth <-
|
||||
Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
|
||||
%User{} = user <- User.get_by_id(auth.user_id),
|
||||
{:ok, token} <- Token.exchange_token(app, auth),
|
||||
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
|
||||
response = %{
|
||||
|
|
@ -81,7 +135,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
refresh_token: token.refresh_token,
|
||||
created_at: DateTime.to_unix(inserted_at),
|
||||
expires_in: 60 * 10,
|
||||
scope: "read write follow"
|
||||
scope: Enum.join(token.scopes, " "),
|
||||
me: user.ap_id
|
||||
}
|
||||
|
||||
json(conn, response)
|
||||
|
|
@ -92,27 +147,42 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
end
|
||||
end
|
||||
|
||||
# TODO
|
||||
# - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
|
||||
def token_exchange(
|
||||
conn,
|
||||
%{"grant_type" => "password", "username" => name, "password" => password} = params
|
||||
%{"grant_type" => "password"} = params
|
||||
) do
|
||||
with %App{} = app <- get_app_from_request(conn, params),
|
||||
%User{} = user <- User.get_by_nickname_or_email(name),
|
||||
true <- Pbkdf2.checkpw(password, user.password_hash),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user),
|
||||
with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn, params)},
|
||||
%App{} = app <- get_app_from_request(conn, params),
|
||||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
|
||||
{:user_active, true} <- {:user_active, !user.info.deactivated},
|
||||
scopes <- oauth_scopes(params, app.scopes),
|
||||
[] <- scopes -- app.scopes,
|
||||
true <- Enum.any?(scopes),
|
||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
|
||||
{:ok, token} <- Token.exchange_token(app, auth) do
|
||||
response = %{
|
||||
token_type: "Bearer",
|
||||
access_token: token.token,
|
||||
refresh_token: token.refresh_token,
|
||||
expires_in: 60 * 10,
|
||||
scope: "read write follow"
|
||||
scope: Enum.join(token.scopes, " "),
|
||||
me: user.ap_id
|
||||
}
|
||||
|
||||
json(conn, response)
|
||||
else
|
||||
{:auth_active, false} ->
|
||||
# Per https://github.com/tootsuite/mastodon/blob/
|
||||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Your login is missing a confirmed e-mail address"})
|
||||
|
||||
{:user_active, false} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{error: "Your account is currently disabled"})
|
||||
|
||||
_error ->
|
||||
put_status(conn, 400)
|
||||
|> json(%{error: "Invalid credentials"})
|
||||
|
|
@ -121,7 +191,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
|
||||
def token_exchange(
|
||||
conn,
|
||||
%{"grant_type" => "password", "name" => name, "password" => password} = params
|
||||
%{"grant_type" => "password", "name" => name, "password" => _password} = params
|
||||
) do
|
||||
params =
|
||||
params
|
||||
|
|
@ -143,13 +213,191 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Prepares OAuth request to provider for Ueberauth"
|
||||
def prepare_request(conn, %{"provider" => provider} = params) do
|
||||
scope =
|
||||
oauth_scopes(params, [])
|
||||
|> Enum.join(" ")
|
||||
|
||||
state =
|
||||
params
|
||||
|> Map.delete("scopes")
|
||||
|> Map.put("scope", scope)
|
||||
|> Poison.encode!()
|
||||
|
||||
params =
|
||||
params
|
||||
|> Map.drop(~w(scope scopes client_id redirect_uri))
|
||||
|> Map.put("state", state)
|
||||
|
||||
# Handing the request to Ueberauth
|
||||
redirect(conn, to: o_auth_path(conn, :request, provider, params))
|
||||
end
|
||||
|
||||
def request(conn, params) do
|
||||
message =
|
||||
if params["provider"] do
|
||||
"Unsupported OAuth provider: #{params["provider"]}."
|
||||
else
|
||||
"Bad OAuth request."
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_flash(:error, message)
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do
|
||||
params = callback_params(params)
|
||||
messages = for e <- Map.get(failure, :errors, []), do: e.message
|
||||
message = Enum.join(messages, "; ")
|
||||
|
||||
conn
|
||||
|> put_flash(:error, "Failed to authenticate: #{message}.")
|
||||
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
|
||||
end
|
||||
|
||||
def callback(conn, params) do
|
||||
params = callback_params(params)
|
||||
|
||||
with {:ok, registration} <- Authenticator.get_registration(conn, params) do
|
||||
user = Repo.preload(registration, :user).user
|
||||
auth_params = Map.take(params, ~w(client_id redirect_uri scope scopes state))
|
||||
|
||||
if user do
|
||||
create_authorization(
|
||||
conn,
|
||||
%{"authorization" => auth_params},
|
||||
user: user
|
||||
)
|
||||
else
|
||||
registration_params =
|
||||
Map.merge(auth_params, %{
|
||||
"nickname" => Registration.nickname(registration),
|
||||
"email" => Registration.email(registration)
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_session(:registration_id, registration.id)
|
||||
|> registration_details(registration_params)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_flash(:error, "Failed to set up user account.")
|
||||
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
|
||||
end
|
||||
end
|
||||
|
||||
defp callback_params(%{"state" => state} = params) do
|
||||
Map.merge(params, Poison.decode!(state))
|
||||
end
|
||||
|
||||
def registration_details(conn, params) do
|
||||
render(conn, "register.html", %{
|
||||
client_id: params["client_id"],
|
||||
redirect_uri: params["redirect_uri"],
|
||||
state: params["state"],
|
||||
scopes: oauth_scopes(params, []),
|
||||
nickname: params["nickname"],
|
||||
email: params["email"]
|
||||
})
|
||||
end
|
||||
|
||||
def register(conn, %{"op" => "connect"} = params) do
|
||||
authorization_params = Map.put(params, "name", params["auth_name"])
|
||||
create_authorization_params = %{"authorization" => authorization_params}
|
||||
|
||||
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, create_authorization_params)},
|
||||
%User{} = user <- Repo.preload(auth, :user).user,
|
||||
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
|
||||
conn
|
||||
|> put_session_registration_id(nil)
|
||||
|> after_create_authorization(auth, authorization_params)
|
||||
else
|
||||
{:create_authorization, error} ->
|
||||
{:register, handle_create_authorization_error(conn, error, create_authorization_params)}
|
||||
|
||||
_ ->
|
||||
{:register, :generic_error}
|
||||
end
|
||||
end
|
||||
|
||||
def register(conn, %{"op" => "register"} = params) do
|
||||
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
|
||||
%Registration{} = registration <- Repo.get(Registration, registration_id),
|
||||
{:ok, user} <- Authenticator.create_from_registration(conn, params, registration) do
|
||||
conn
|
||||
|> put_session_registration_id(nil)
|
||||
|> create_authorization(
|
||||
%{
|
||||
"authorization" => %{
|
||||
"client_id" => params["client_id"],
|
||||
"redirect_uri" => params["redirect_uri"],
|
||||
"scopes" => oauth_scopes(params, nil)
|
||||
}
|
||||
},
|
||||
user: user
|
||||
)
|
||||
else
|
||||
{:error, changeset} ->
|
||||
message =
|
||||
Enum.map(changeset.errors, fn {field, {error, _}} ->
|
||||
"#{field} #{error}"
|
||||
end)
|
||||
|> Enum.join("; ")
|
||||
|
||||
message =
|
||||
String.replace(
|
||||
message,
|
||||
"ap_id has already been taken",
|
||||
"nickname has already been taken"
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> put_flash(:error, "Error: #{message}.")
|
||||
|> registration_details(params)
|
||||
|
||||
_ ->
|
||||
{:register, :generic_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create_authorization(
|
||||
conn,
|
||||
%{
|
||||
"authorization" =>
|
||||
%{
|
||||
"client_id" => client_id,
|
||||
"redirect_uri" => redirect_uri
|
||||
} = auth_params
|
||||
} = params,
|
||||
user \\ nil
|
||||
) do
|
||||
with {_, {:ok, %User{} = user}} <-
|
||||
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn, params)},
|
||||
%App{} = app <- Repo.get_by(App, client_id: client_id),
|
||||
true <- redirect_uri in String.split(app.redirect_uris),
|
||||
scopes <- oauth_scopes(auth_params, []),
|
||||
{:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes},
|
||||
# Note: `scope` param is intentionally not optional in this context
|
||||
{:missing_scopes, false} <- {:missing_scopes, scopes == []},
|
||||
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
|
||||
Authorization.create_authorization(app, user, scopes)
|
||||
end
|
||||
end
|
||||
|
||||
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
|
||||
# decoding it. Investigate sometime.
|
||||
defp fix_padding(token) do
|
||||
token
|
||||
|> URI.decode()
|
||||
|> Base.url_decode64!(padding: false)
|
||||
|> Base.url_encode64()
|
||||
|> Base.url_encode64(padding: false)
|
||||
end
|
||||
|
||||
defp get_app_from_request(conn, params) do
|
||||
|
|
@ -175,4 +423,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Special case: Local MastodonFE
|
||||
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
|
||||
|
||||
defp redirect_uri(_conn, redirect_uri), do: redirect_uri
|
||||
|
||||
defp get_session_registration_id(conn), do: get_session(conn, :registration_id)
|
||||
|
||||
defp put_session_registration_id(conn, registration_id),
|
||||
do: put_session(conn, :registration_id, registration_id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.OAuthView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OAuth.Token do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.{User, Repo}
|
||||
alias Pleroma.Web.OAuth.{Token, App, Authorization}
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
|
||||
schema "oauth_tokens" do
|
||||
field(:token, :string)
|
||||
field(:refresh_token, :string)
|
||||
field(:valid_until, :naive_datetime)
|
||||
belongs_to(:user, Pleroma.User)
|
||||
field(:scopes, {:array, :string}, default: [])
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
belongs_to(:user, Pleroma.User, type: Pleroma.FlakeId)
|
||||
belongs_to(:app, App)
|
||||
|
||||
timestamps()
|
||||
|
|
@ -19,17 +27,19 @@ defmodule Pleroma.Web.OAuth.Token do
|
|||
def exchange_token(app, auth) do
|
||||
with {:ok, auth} <- Authorization.use_token(auth),
|
||||
true <- auth.app_id == app.id do
|
||||
create_token(app, Repo.get(User, auth.user_id))
|
||||
create_token(app, User.get_by_id(auth.user_id), auth.scopes)
|
||||
end
|
||||
end
|
||||
|
||||
def create_token(%App{} = app, %User{} = user) do
|
||||
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
|
||||
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
|
||||
def create_token(%App{} = app, %User{} = user, scopes \\ nil) do
|
||||
scopes = scopes || app.scopes
|
||||
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
||||
|
||||
token = %Token{
|
||||
token: token,
|
||||
refresh_token: refresh_token,
|
||||
scopes: scopes,
|
||||
user_id: user.id,
|
||||
app_id: app.id,
|
||||
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
|
||||
|
|
@ -40,9 +50,27 @@ defmodule Pleroma.Web.OAuth.Token do
|
|||
|
||||
def delete_user_tokens(%User{id: user_id}) do
|
||||
from(
|
||||
t in Pleroma.Web.OAuth.Token,
|
||||
t in Token,
|
||||
where: t.user_id == ^user_id
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def delete_user_token(%User{id: user_id}, token_id) do
|
||||
from(
|
||||
t in Token,
|
||||
where: t.user_id == ^user_id,
|
||||
where: t.id == ^token_id
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def get_user_tokens(%User{id: user_id}) do
|
||||
from(
|
||||
t in Token,
|
||||
where: t.user_id == ^user_id
|
||||
)
|
||||
|> Repo.all()
|
||||
|> Repo.preload(:app)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
|
||||
alias Pleroma.{Activity, User, Object}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OStatus.UserRepresenter
|
||||
|
||||
require Logger
|
||||
|
||||
defp get_href(id) do
|
||||
|
|
@ -173,7 +180,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
|
|||
_in_reply_to = get_in_reply_to(activity.data)
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
|
||||
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
|
||||
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
|
||||
|
||||
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.FeedRepresenter do
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.OStatus.UserRepresenter
|
||||
|
||||
def to_simple_form(user, activities, _users) do
|
||||
most_recent_update =
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.DeleteHandler do
|
||||
require Logger
|
||||
alias Pleroma.Web.XML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle_delete(entry, _doc \\ nil) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.FollowHandler do
|
||||
alias Pleroma.Web.{XML, OStatus}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle(entry, doc) do
|
||||
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.NoteHandler do
|
||||
require Logger
|
||||
alias Pleroma.Web.{XML, OStatus}
|
||||
alias Pleroma.{Object, Activity}
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
@doc """
|
||||
Get the context for this note. Uses this:
|
||||
|
|
@ -12,13 +19,13 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
|
|||
2. The conversation reference in the ostatus xml
|
||||
3. A newly generated context id.
|
||||
"""
|
||||
def get_context(entry, inReplyTo) do
|
||||
def get_context(entry, in_reply_to) do
|
||||
context =
|
||||
(XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
|
||||
XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|
||||
|> String.trim()
|
||||
|
||||
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
|
||||
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do
|
||||
context
|
||||
else
|
||||
_e ->
|
||||
|
|
@ -81,14 +88,14 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
|
|||
Map.put(note, "external_url", url)
|
||||
end
|
||||
|
||||
def fetch_replied_to_activity(entry, inReplyTo) do
|
||||
with %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(inReplyTo) do
|
||||
def fetch_replied_to_activity(entry, in_reply_to) do
|
||||
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
|
||||
activity
|
||||
else
|
||||
_e ->
|
||||
with inReplyToHref when not is_nil(inReplyToHref) <-
|
||||
with in_reply_to_href when not is_nil(in_reply_to_href) <-
|
||||
XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
|
||||
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(inReplyToHref) do
|
||||
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do
|
||||
activity
|
||||
else
|
||||
_e -> nil
|
||||
|
|
@ -99,18 +106,18 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
|
|||
# TODO: Clean this up a bit.
|
||||
def handle_note(entry, doc \\ nil) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id),
|
||||
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),
|
||||
content_html <- OStatus.get_content(entry),
|
||||
cw <- OStatus.get_cw(entry),
|
||||
inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
|
||||
inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo),
|
||||
inReplyToObject <-
|
||||
(inReplyToActivity && Object.normalize(inReplyToActivity.data["object"])) || nil,
|
||||
inReplyTo <- (inReplyToObject && inReplyToObject.data["id"]) || inReplyTo,
|
||||
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
|
||||
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
|
||||
in_reply_to_object <-
|
||||
(in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
|
||||
in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
|
||||
attachments <- OStatus.get_attachments(entry),
|
||||
context <- get_context(entry, inReplyTo),
|
||||
context <- get_context(entry, in_reply_to),
|
||||
tags <- OStatus.get_tags(entry),
|
||||
mentions <- get_mentions(entry),
|
||||
to <- make_to_list(actor, mentions),
|
||||
|
|
@ -124,7 +131,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
|
|||
context,
|
||||
content_html,
|
||||
attachments,
|
||||
inReplyToActivity,
|
||||
in_reply_to_activity,
|
||||
[],
|
||||
cw
|
||||
),
|
||||
|
|
@ -136,8 +143,8 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
|
|||
# TODO: Handle this case in make_note_data
|
||||
note <-
|
||||
if(
|
||||
inReplyTo && !inReplyToActivity,
|
||||
do: note |> Map.put("inReplyTo", inReplyTo),
|
||||
in_reply_to && !in_reply_to_activity,
|
||||
do: note |> Map.put("inReplyTo", in_reply_to),
|
||||
else: note
|
||||
) do
|
||||
ActivityPub.create(%{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.UnfollowHandler do
|
||||
alias Pleroma.Web.{XML, OStatus}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle(entry, doc) do
|
||||
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus do
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
|
||||
|
|
@ -5,14 +9,22 @@ defmodule Pleroma.Web.OStatus do
|
|||
import Pleroma.Web.XML
|
||||
require Logger
|
||||
|
||||
alias Pleroma.{Repo, User, Web, Object, Activity}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.{WebFinger, Websub}
|
||||
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.OStatus.DeleteHandler
|
||||
alias Pleroma.Web.OStatus.FollowHandler
|
||||
alias Pleroma.Web.OStatus.NoteHandler
|
||||
alias Pleroma.Web.OStatus.UnfollowHandler
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Pleroma.Web.Websub
|
||||
|
||||
def is_representable?(%Activity{data: data}) do
|
||||
object = Object.normalize(data["object"])
|
||||
def is_representable?(%Activity{} = activity) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cond do
|
||||
is_nil(object) ->
|
||||
|
|
@ -44,6 +56,9 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
def handle_incoming(xml_string) do
|
||||
with doc when doc != :error <- parse_document(xml_string) do
|
||||
with {:ok, actor_user} <- find_make_or_update_user(doc),
|
||||
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
|
||||
|
||||
entries = :xmerl_xpath.string('//entry', doc)
|
||||
|
||||
activities =
|
||||
|
|
@ -104,7 +119,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
def make_share(entry, doc, retweeted_activity) do
|
||||
with {:ok, actor} <- find_make_or_update_user(doc),
|
||||
%Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
|
||||
%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
|
||||
{:ok, activity}
|
||||
|
|
@ -122,7 +137,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
def make_favorite(entry, doc, favorited_activity) do
|
||||
with {:ok, actor} <- find_make_or_update_user(doc),
|
||||
%Object{} = object <- Object.normalize(favorited_activity.data["object"]),
|
||||
%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
|
||||
{:ok, activity}
|
||||
|
|
@ -144,7 +159,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
Logger.debug("Trying to get entry from db")
|
||||
|
||||
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ ->
|
||||
|
|
@ -346,13 +361,10 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
def fetch_activity_from_atom_url(url) do
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
@httpoison.get(
|
||||
url,
|
||||
[Accept: "application/atom+xml"],
|
||||
follow_redirect: true,
|
||||
timeout: 10000,
|
||||
recv_timeout: 20000
|
||||
[{:Accept, "application/atom+xml"}]
|
||||
) do
|
||||
Logger.debug("Got document from #{url}, handling...")
|
||||
handle_incoming(body)
|
||||
|
|
@ -367,8 +379,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
Logger.debug("Trying to fetch #{url}")
|
||||
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body}} <-
|
||||
@httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
|
||||
{:ok, %{body: body}} <- @httpoison.get(url, []),
|
||||
{:ok, atom_url} <- get_atom_url(body) do
|
||||
fetch_activity_from_atom_url(atom_url)
|
||||
else
|
||||
|
|
@ -379,19 +390,14 @@ defmodule Pleroma.Web.OStatus do
|
|||
end
|
||||
|
||||
def fetch_activity_from_url(url) do
|
||||
try do
|
||||
with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e ->
|
||||
with {:ok, activities} <- fetch_activity_from_html_url(url) do
|
||||
{:ok, activities}
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e -> fetch_activity_from_html_url(url)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +1,42 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.OStatusController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.{User, Activity, Object}
|
||||
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.{OStatus, Federator}
|
||||
alias Pleroma.Web.XML
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.ActivityPubController
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.ActivityPubController
|
||||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.OStatus.FeedRepresenter
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
|
||||
|
||||
action_fallback(:errors)
|
||||
|
||||
def feed_redirect(conn, %{"nickname" => nickname}) do
|
||||
case get_format(conn) do
|
||||
"html" ->
|
||||
Fallback.RedirectController.redirector(conn, nil)
|
||||
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))
|
||||
|
|
@ -75,20 +91,20 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
{:ok, body, _conn} = read_body(conn)
|
||||
{:ok, doc} = decode_or_retry(body)
|
||||
|
||||
Federator.enqueue(:incoming_doc, doc)
|
||||
Federator.incoming_doc(doc)
|
||||
|
||||
conn
|
||||
|> send_resp(200, "")
|
||||
end
|
||||
|
||||
def object(conn, %{"uuid" => uuid}) do
|
||||
if get_format(conn) == "activity+json" 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_activity_by_object_ap_id(id)},
|
||||
{_, true} <- {:public?, ActivityPub.is_public?(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}")
|
||||
|
|
@ -108,65 +124,110 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
end
|
||||
|
||||
def activity(conn, %{"uuid" => uuid}) do
|
||||
with id <- o_status_url(conn, :activity, uuid),
|
||||
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
|
||||
{_, true} <- {:public?, ActivityPub.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
|
||||
if get_format(conn) in ["activity+json", "json"] do
|
||||
ActivityPubController.call(conn, :activity)
|
||||
else
|
||||
{:public?, false} ->
|
||||
{:error, :not_found}
|
||||
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}
|
||||
|
||||
{:activity, nil} ->
|
||||
{:error, :not_found}
|
||||
{:activity, nil} ->
|
||||
{:error, :not_found}
|
||||
|
||||
e ->
|
||||
e
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notice(conn, %{"id" => id}) do
|
||||
with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)},
|
||||
{_, true} <- {:public?, ActivityPub.is_public?(activity)},
|
||||
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" ->
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
|
||||
if activity.data["type"] == "Create" do
|
||||
%Object{} = object = Object.normalize(activity)
|
||||
|
||||
Fallback.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
|
||||
),
|
||||
user: user
|
||||
})
|
||||
else
|
||||
Fallback.RedirectController.redirector(conn, nil)
|
||||
end
|
||||
|
||||
_ ->
|
||||
represent_activity(conn, format, activity, user)
|
||||
end
|
||||
else
|
||||
{:public?, false} ->
|
||||
{:error, :not_found}
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> Fallback.RedirectController.redirector(nil, 404)
|
||||
|
||||
{:activity, nil} ->
|
||||
{:error, :not_found}
|
||||
conn
|
||||
|> Fallback.RedirectController.redirector(nil, 404)
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
|
||||
def notice_player(conn, %{"id" => id}) do
|
||||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
|
||||
true <- Visibility.is_public?(activity),
|
||||
%Object{} = object <- Object.normalize(activity),
|
||||
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
|
||||
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
|
||||
conn
|
||||
|> put_layout(:metadata_player)
|
||||
|> put_resp_header("x-frame-options", "ALLOW")
|
||||
|> put_resp_header(
|
||||
"content-security-policy",
|
||||
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
|
||||
)
|
||||
|> put_view(Pleroma.Web.Metadata.PlayerView)
|
||||
|> render("player.html", url)
|
||||
else
|
||||
_error ->
|
||||
conn
|
||||
|> put_status(404)
|
||||
|> Fallback.RedirectController.redirector(nil, 404)
|
||||
end
|
||||
end
|
||||
|
||||
defp represent_activity(
|
||||
conn,
|
||||
"activity+json",
|
||||
%Activity{data: %{"type" => "Create"}} = activity,
|
||||
user
|
||||
_user
|
||||
) do
|
||||
object = Object.normalize(activity.data["object"])
|
||||
object = Object.normalize(activity)
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ObjectView.render("object.json", %{object: object}))
|
||||
end
|
||||
|
||||
defp represent_activity(conn, "activity+json", _, _) do
|
||||
defp represent_activity(_conn, "activity+json", _, _) do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.UserRepresenter do
|
||||
alias Pleroma.User
|
||||
|
||||
|
|
|
|||
133
lib/pleroma/web/push/impl.ex
Normal file
133
lib/pleroma/web/push/impl.ex
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Push.Impl do
|
||||
@moduledoc "The module represents implementation push web notification"
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Metadata.Utils
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
||||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like"]
|
||||
|
||||
@doc "Performs sending notifications for user subscriptions"
|
||||
@spec perform(Notification.t()) :: list(any) | :error
|
||||
def perform(
|
||||
%{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} =
|
||||
notif
|
||||
)
|
||||
when activity_type in @types do
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
|
||||
type = Activity.mastodon_notification_type(notif.activity)
|
||||
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
|
||||
avatar_url = User.avatar_url(actor)
|
||||
|
||||
for subscription <- fetch_subsriptions(user_id),
|
||||
get_in(subscription.data, ["alerts", type]) do
|
||||
%{
|
||||
title: format_title(notif),
|
||||
access_token: subscription.token.token,
|
||||
body: format_body(notif, actor),
|
||||
notification_id: notif.id,
|
||||
notification_type: type,
|
||||
icon: avatar_url,
|
||||
preferred_locale: "en",
|
||||
pleroma: %{
|
||||
activity_id: activity_id
|
||||
}
|
||||
}
|
||||
|> Jason.encode!()
|
||||
|> push_message(build_sub(subscription), gcm_api_key, subscription)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(_) do
|
||||
Logger.warn("Unknown notification type")
|
||||
:error
|
||||
end
|
||||
|
||||
@doc "Push message to web"
|
||||
def push_message(body, sub, api_key, subscription) do
|
||||
case WebPushEncryption.send_web_push(body, sub, api_key) do
|
||||
{:ok, %{status_code: code}} when 400 <= code and code < 500 ->
|
||||
Logger.debug("Removing subscription record")
|
||||
Repo.delete!(subscription)
|
||||
:ok
|
||||
|
||||
{:ok, %{status_code: code}} when 200 <= code and code < 300 ->
|
||||
:ok
|
||||
|
||||
{:ok, %{status_code: code}} ->
|
||||
Logger.error("Web Push Notification failed with code: #{code}")
|
||||
:error
|
||||
|
||||
_ ->
|
||||
Logger.error("Web Push Notification failed with unknown error")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Gets user subscriptions"
|
||||
def fetch_subsriptions(user_id) do
|
||||
Subscription
|
||||
|> where(user_id: ^user_id)
|
||||
|> preload(:token)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def build_sub(subscription) do
|
||||
%{
|
||||
keys: %{
|
||||
p256dh: subscription.key_p256dh,
|
||||
auth: subscription.key_auth
|
||||
},
|
||||
endpoint: subscription.endpoint
|
||||
}
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}},
|
||||
actor
|
||||
) do
|
||||
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Announce", "object" => activity_id}}},
|
||||
actor
|
||||
) do
|
||||
%Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
|
||||
%Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)
|
||||
|
||||
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => type}}},
|
||||
actor
|
||||
)
|
||||
when type in ["Follow", "Like"] do
|
||||
case type do
|
||||
"Follow" -> "@#{actor.nickname} has followed you"
|
||||
"Like" -> "@#{actor.nickname} has favorited your post"
|
||||
end
|
||||
end
|
||||
|
||||
def format_title(%{activity: %{data: %{"type" => type}}}) do
|
||||
case type do
|
||||
"Create" -> "New Mention"
|
||||
"Follow" -> "New Follower"
|
||||
"Announce" -> "New Repeat"
|
||||
"Like" -> "New Favorite"
|
||||
end
|
||||
end
|
||||
end
|
||||
36
lib/pleroma/web/push/push.ex
Normal file
36
lib/pleroma/web/push/push.ex
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Push do
|
||||
alias Pleroma.Web.Push.Impl
|
||||
|
||||
require Logger
|
||||
|
||||
def init do
|
||||
unless enabled() do
|
||||
Logger.warn("""
|
||||
VAPID key pair is not found. If you wish to enabled web push, please run
|
||||
|
||||
mix web_push.gen.keypair
|
||||
|
||||
and add the resulting output to your configuration file.
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
def vapid_config do
|
||||
Application.get_env(:web_push_encryption, :vapid_details, [])
|
||||
end
|
||||
|
||||
def enabled do
|
||||
case vapid_config() do
|
||||
[] -> false
|
||||
list when is_list(list) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def send(notification),
|
||||
do: PleromaJobQueue.enqueue(:web_push, Impl, [notification])
|
||||
end
|
||||
93
lib/pleroma/web/push/subscription.ex
Normal file
93
lib/pleroma/web/push/subscription.ex
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Push.Subscription do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
schema "push_subscriptions" do
|
||||
belongs_to(:user, User, type: Pleroma.FlakeId)
|
||||
belongs_to(:token, Token)
|
||||
field(:endpoint, :string)
|
||||
field(:key_p256dh, :string)
|
||||
field(:key_auth, :string)
|
||||
field(:data, :map, default: %{})
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@supported_alert_types ~w[follow favourite mention reblog]
|
||||
|
||||
defp alerts(%{"data" => %{"alerts" => alerts}}) do
|
||||
alerts = Map.take(alerts, @supported_alert_types)
|
||||
%{"alerts" => alerts}
|
||||
end
|
||||
|
||||
def create(
|
||||
%User{} = user,
|
||||
%Token{} = token,
|
||||
%{
|
||||
"subscription" => %{
|
||||
"endpoint" => endpoint,
|
||||
"keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
|
||||
}
|
||||
} = params
|
||||
) do
|
||||
Repo.insert(%Subscription{
|
||||
user_id: user.id,
|
||||
token_id: token.id,
|
||||
endpoint: endpoint,
|
||||
key_auth: ensure_base64_urlsafe(key_auth),
|
||||
key_p256dh: ensure_base64_urlsafe(key_p256dh),
|
||||
data: alerts(params)
|
||||
})
|
||||
end
|
||||
|
||||
@doc "Gets subsciption by user & token"
|
||||
@spec get(User.t(), Token.t()) :: {:ok, t()} | {:error, :not_found}
|
||||
def get(%User{id: user_id}, %Token{id: token_id}) do
|
||||
case Repo.get_by(Subscription, user_id: user_id, token_id: token_id) do
|
||||
nil -> {:error, :not_found}
|
||||
subscription -> {:ok, subscription}
|
||||
end
|
||||
end
|
||||
|
||||
def update(user, token, params) do
|
||||
with {:ok, subscription} <- get(user, token) do
|
||||
subscription
|
||||
|> change(data: alerts(params))
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
def delete(user, token) do
|
||||
with {:ok, subscription} <- get(user, token),
|
||||
do: Repo.delete(subscription)
|
||||
end
|
||||
|
||||
def delete_if_exists(user, token) do
|
||||
case get(user, token) do
|
||||
{:error, _} -> {:ok, nil}
|
||||
{:ok, sub} -> Repo.delete(sub)
|
||||
end
|
||||
end
|
||||
|
||||
# Some webpush clients (e.g. iOS Toot!) use an non urlsafe base64 as an encoding for the key.
|
||||
# However, the web push rfs specify to use base64 urlsafe, and the `web_push_encryption` library
|
||||
# we use requires the key to be properly encoded. So we just convert base64 to urlsafe base64.
|
||||
defp ensure_base64_urlsafe(string) do
|
||||
string
|
||||
|> String.replace("+", "-")
|
||||
|> String.replace("/", "_")
|
||||
|> String.replace("=", "")
|
||||
end
|
||||
end
|
||||
52
lib/pleroma/web/rel_me.ex
Normal file
52
lib/pleroma/web/rel_me.ex
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RelMe do
|
||||
@hackney_options [
|
||||
pool: :media,
|
||||
recv_timeout: 2_000,
|
||||
max_body: 2_000_000,
|
||||
with_body: true
|
||||
]
|
||||
|
||||
if Mix.env() == :test do
|
||||
def parse(url) when is_binary(url), do: parse_url(url)
|
||||
else
|
||||
def parse(url) when is_binary(url) do
|
||||
Cachex.fetch!(:rel_me_cache, url, fn _ ->
|
||||
{:commit, parse_url(url)}
|
||||
end)
|
||||
rescue
|
||||
e -> {:error, "Cachex error: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse(_), do: {:error, "No URL provided"}
|
||||
|
||||
defp parse_url(url) do
|
||||
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
|
||||
|
||||
data =
|
||||
Floki.attribute(html, "link[rel~=me]", "href") ++
|
||||
Floki.attribute(html, "a[rel~=me]", "href")
|
||||
|
||||
{:ok, data}
|
||||
rescue
|
||||
e -> {:error, "Parsing error: #{inspect(e)}"}
|
||||
end
|
||||
|
||||
def maybe_put_rel_me("http" <> _ = target_page, profile_urls) when is_list(profile_urls) do
|
||||
{:ok, rel_me_hrefs} = parse(target_page)
|
||||
|
||||
true = Enum.any?(rel_me_hrefs, fn x -> x in profile_urls end)
|
||||
|
||||
"me"
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
def maybe_put_rel_me(_, _) do
|
||||
nil
|
||||
end
|
||||
end
|
||||
37
lib/pleroma/web/rich_media/helpers.ex
Normal file
37
lib/pleroma/web/rich_media/helpers.ex
Normal 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.RichMedia.Helpers do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.RichMedia.Parser
|
||||
|
||||
defp validate_page_url(page_url) when is_binary(page_url) do
|
||||
if AutoLinker.Parser.is_url?(page_url, true) do
|
||||
URI.parse(page_url) |> validate_page_url
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_page_url(%URI{authority: nil}), do: :error
|
||||
defp validate_page_url(%URI{scheme: nil}), do: :error
|
||||
defp validate_page_url(%URI{}), do: :ok
|
||||
defp validate_page_url(_), do: :error
|
||||
|
||||
def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
|
||||
with true <- Pleroma.Config.get([:rich_media, :enabled]),
|
||||
%Object{} = object <- Object.normalize(activity),
|
||||
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
|
||||
:ok <- validate_page_url(page_url),
|
||||
{:ok, rich_media} <- Parser.parse(page_url) do
|
||||
%{page_url: page_url, rich_media: rich_media}
|
||||
else
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_data_for_activity(_), do: %{}
|
||||
end
|
||||
75
lib/pleroma/web/rich_media/parser.ex
Normal file
75
lib/pleroma/web/rich_media/parser.ex
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.RichMedia.Parser do
|
||||
@parsers [
|
||||
Pleroma.Web.RichMedia.Parsers.OGP,
|
||||
Pleroma.Web.RichMedia.Parsers.TwitterCard,
|
||||
Pleroma.Web.RichMedia.Parsers.OEmbed
|
||||
]
|
||||
|
||||
@hackney_options [
|
||||
pool: :media,
|
||||
recv_timeout: 2_000,
|
||||
max_body: 2_000_000,
|
||||
with_body: true
|
||||
]
|
||||
|
||||
def parse(nil), do: {:error, "No URL provided"}
|
||||
|
||||
if Mix.env() == :test do
|
||||
def parse(url), do: parse_url(url)
|
||||
else
|
||||
def parse(url) do
|
||||
try do
|
||||
Cachex.fetch!(:rich_media_cache, url, fn _ ->
|
||||
{:commit, parse_url(url)}
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
{:error, "Cachex error: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_url(url) do
|
||||
try do
|
||||
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
|
||||
|
||||
html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data()
|
||||
rescue
|
||||
e ->
|
||||
{:error, "Parsing error: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_parse(html) do
|
||||
Enum.reduce_while(@parsers, %{}, fn parser, acc ->
|
||||
case parser.parse(html, acc) do
|
||||
{:ok, data} -> {:halt, data}
|
||||
{:error, _msg} -> {:cont, acc}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp check_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
defp check_parsed_data(data) do
|
||||
{:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
|
||||
end
|
||||
|
||||
defp clean_parsed_data(data) do
|
||||
data
|
||||
|> Enum.reject(fn {key, val} ->
|
||||
with {:ok, _} <- Jason.encode(%{key => val}) do
|
||||
false
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
end)
|
||||
|> Map.new()
|
||||
end
|
||||
end
|
||||
30
lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
Normal file
30
lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do
|
||||
def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do
|
||||
with elements = [_ | _] <- get_elements(html, key_name, prefix),
|
||||
meta_data =
|
||||
Enum.reduce(elements, data, fn el, acc ->
|
||||
attributes = normalize_attributes(el, prefix, key_name, value_name)
|
||||
|
||||
Map.merge(acc, attributes)
|
||||
end) do
|
||||
{:ok, meta_data}
|
||||
else
|
||||
_e -> {:error, error_message}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_elements(html, key_name, prefix) do
|
||||
html |> Floki.find("meta[#{key_name}^='#{prefix}:']")
|
||||
end
|
||||
|
||||
defp normalize_attributes(html_node, prefix, key_name, value_name) do
|
||||
{_tag, attributes, _children} = html_node
|
||||
|
||||
data =
|
||||
Enum.into(attributes, %{}, fn {name, value} ->
|
||||
{name, String.trim_leading(value, "#{prefix}:")}
|
||||
end)
|
||||
|
||||
%{String.to_atom(data[key_name]) => data[value_name]}
|
||||
end
|
||||
end
|
||||
31
lib/pleroma/web/rich_media/parsers/oembed_parser.ex
Normal file
31
lib/pleroma/web/rich_media/parsers/oembed_parser.ex
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
|
||||
def parse(html, _data) do
|
||||
with elements = [_ | _] <- get_discovery_data(html),
|
||||
{:ok, oembed_url} <- get_oembed_url(elements),
|
||||
{:ok, oembed_data} <- get_oembed_data(oembed_url) do
|
||||
{:ok, oembed_data}
|
||||
else
|
||||
_e -> {:error, "No OEmbed data found"}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_discovery_data(html) do
|
||||
html |> Floki.find("link[type='application/json+oembed']")
|
||||
end
|
||||
|
||||
defp get_oembed_url(nodes) do
|
||||
{"link", attributes, _children} = nodes |> hd()
|
||||
|
||||
{:ok, Enum.into(attributes, %{})["href"]}
|
||||
end
|
||||
|
||||
defp get_oembed_data(url) do
|
||||
{:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
|
||||
|
||||
{:ok, data} = Jason.decode(json)
|
||||
|
||||
data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
|
||||
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
||||
11
lib/pleroma/web/rich_media/parsers/ogp.ex
Normal file
11
lib/pleroma/web/rich_media/parsers/ogp.ex
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
|
||||
def parse(html, data) do
|
||||
Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
|
||||
html,
|
||||
data,
|
||||
"og",
|
||||
"No OGP metadata found",
|
||||
"property"
|
||||
)
|
||||
end
|
||||
end
|
||||
11
lib/pleroma/web/rich_media/parsers/twitter_card.ex
Normal file
11
lib/pleroma/web/rich_media/parsers/twitter_card.ex
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
|
||||
def parse(html, data) do
|
||||
Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
|
||||
html,
|
||||
data,
|
||||
"twitter",
|
||||
"No twitter card metadata found",
|
||||
"name"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Router do
|
||||
use Pleroma.Web, :router
|
||||
|
||||
alias Pleroma.{Repo, User, Web.Router}
|
||||
pipeline :browser do
|
||||
plug(:accepts, ["html"])
|
||||
plug(:fetch_session)
|
||||
end
|
||||
|
||||
pipeline :oauth do
|
||||
plug(:fetch_session)
|
||||
plug(Pleroma.Plugs.OAuthPlug)
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug(:accepts, ["json"])
|
||||
|
|
@ -40,6 +52,7 @@ defmodule Pleroma.Web.Router do
|
|||
plug(Pleroma.Plugs.SessionAuthenticationPlug)
|
||||
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
|
||||
plug(Pleroma.Plugs.AuthenticationPlug)
|
||||
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
|
||||
plug(Pleroma.Plugs.UserEnabledPlug)
|
||||
plug(Pleroma.Plugs.SetUserSessionIdPlug)
|
||||
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
|
||||
|
|
@ -71,6 +84,29 @@ defmodule Pleroma.Web.Router do
|
|||
plug(Pleroma.Plugs.EnsureUserKeyPlug)
|
||||
end
|
||||
|
||||
pipeline :oauth_read_or_unauthenticated do
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{
|
||||
scopes: ["read"],
|
||||
fallback: :proceed_unauthenticated
|
||||
})
|
||||
end
|
||||
|
||||
pipeline :oauth_read do
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["read"]})
|
||||
end
|
||||
|
||||
pipeline :oauth_write do
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"]})
|
||||
end
|
||||
|
||||
pipeline :oauth_follow do
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["follow"]})
|
||||
end
|
||||
|
||||
pipeline :oauth_push do
|
||||
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
|
||||
end
|
||||
|
||||
pipeline :well_known do
|
||||
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
|
||||
end
|
||||
|
|
@ -79,165 +115,289 @@ defmodule Pleroma.Web.Router do
|
|||
plug(:accepts, ["json", "xml"])
|
||||
end
|
||||
|
||||
pipeline :oauth do
|
||||
plug(:accepts, ["html", "json"])
|
||||
end
|
||||
|
||||
pipeline :pleroma_api do
|
||||
plug(:accepts, ["html", "json"])
|
||||
end
|
||||
|
||||
pipeline :mailbox_preview do
|
||||
plug(:accepts, ["html"])
|
||||
|
||||
plug(:put_secure_browser_headers, %{
|
||||
"content-security-policy" =>
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
})
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
|
||||
pipe_through(:pleroma_api)
|
||||
|
||||
get("/password_reset/:token", UtilController, :show_password_reset)
|
||||
post("/password_reset", UtilController, :password_reset)
|
||||
get("/emoji", UtilController, :emoji)
|
||||
get("/captcha", UtilController, :captcha)
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web do
|
||||
pipe_through(:pleroma_api)
|
||||
post("/uploader_callback/:upload_path", UploaderController, :callback)
|
||||
end
|
||||
|
||||
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
|
||||
pipe_through(:admin_api)
|
||||
pipe_through([:admin_api, :oauth_write])
|
||||
|
||||
post("/user/follow", AdminAPIController, :user_follow)
|
||||
post("/user/unfollow", AdminAPIController, :user_unfollow)
|
||||
|
||||
get("/users", AdminAPIController, :list_users)
|
||||
get("/users/:nickname", AdminAPIController, :user_show)
|
||||
|
||||
delete("/user", AdminAPIController, :user_delete)
|
||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||
post("/user", AdminAPIController, :user_create)
|
||||
put("/users/tag", AdminAPIController, :tag_users)
|
||||
delete("/users/tag", AdminAPIController, :untag_users)
|
||||
|
||||
get("/permission_group/:nickname", AdminAPIController, :right_get)
|
||||
get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
|
||||
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
|
||||
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
|
||||
|
||||
put("/activation_status/:nickname", AdminAPIController, :set_activation_status)
|
||||
|
||||
post("/relay", AdminAPIController, :relay_follow)
|
||||
delete("/relay", AdminAPIController, :relay_unfollow)
|
||||
|
||||
get("/invite_token", AdminAPIController, :get_invite_token)
|
||||
get("/invites", AdminAPIController, :invites)
|
||||
post("/revoke_invite", AdminAPIController, :revoke_invite)
|
||||
post("/email_invite", AdminAPIController, :email_invite)
|
||||
|
||||
get("/password_reset", AdminAPIController, :get_password_reset)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.TwitterAPI do
|
||||
pipe_through(:pleroma_html)
|
||||
get("/ostatus_subscribe", UtilController, :remote_follow)
|
||||
post("/ostatus_subscribe", UtilController, :do_remote_follow)
|
||||
|
||||
post("/main/ostatus", UtilController, :remote_subscribe)
|
||||
get("/ostatus_subscribe", UtilController, :remote_follow)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_follow)
|
||||
post("/ostatus_subscribe", UtilController, :do_remote_follow)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
post("/follow_import", UtilController, :follow_import)
|
||||
post("/change_password", UtilController, :change_password)
|
||||
post("/delete_account", UtilController, :delete_account)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_write)
|
||||
|
||||
post("/change_password", UtilController, :change_password)
|
||||
post("/delete_account", UtilController, :delete_account)
|
||||
put("/notification_settings", UtilController, :update_notificaton_settings)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_follow)
|
||||
|
||||
post("/blocks_import", UtilController, :blocks_import)
|
||||
post("/follow_import", UtilController, :follow_import)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_read)
|
||||
|
||||
post("/notifications/read", UtilController, :notifications_read)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/oauth", Pleroma.Web.OAuth do
|
||||
get("/authorize", OAuthController, :authorize)
|
||||
scope [] do
|
||||
pipe_through(:oauth)
|
||||
get("/authorize", OAuthController, :authorize)
|
||||
end
|
||||
|
||||
post("/authorize", OAuthController, :create_authorization)
|
||||
post("/token", OAuthController, :token_exchange)
|
||||
post("/revoke", OAuthController, :token_revoke)
|
||||
get("/registration_details", OAuthController, :registration_details)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:browser)
|
||||
|
||||
get("/prepare_request", OAuthController, :prepare_request)
|
||||
get("/:provider", OAuthController, :request)
|
||||
get("/:provider/callback", OAuthController, :callback)
|
||||
post("/register", OAuthController, :register)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
|
||||
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
|
||||
get("/accounts/relationships", MastodonAPIController, :relationships)
|
||||
get("/accounts/search", MastodonAPIController, :account_search)
|
||||
post("/accounts/:id/follow", MastodonAPIController, :follow)
|
||||
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
|
||||
post("/accounts/:id/block", MastodonAPIController, :block)
|
||||
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
|
||||
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
|
||||
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
|
||||
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
|
||||
scope [] do
|
||||
pipe_through(:oauth_read)
|
||||
|
||||
get("/follow_requests", MastodonAPIController, :follow_requests)
|
||||
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
|
||||
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
|
||||
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
|
||||
|
||||
post("/follows", MastodonAPIController, :follow)
|
||||
get("/accounts/relationships", MastodonAPIController, :relationships)
|
||||
get("/accounts/search", MastodonAPIController, :account_search)
|
||||
|
||||
get("/blocks", MastodonAPIController, :blocks)
|
||||
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
|
||||
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
|
||||
|
||||
get("/mutes", MastodonAPIController, :empty_array)
|
||||
get("/follow_requests", MastodonAPIController, :follow_requests)
|
||||
get("/blocks", MastodonAPIController, :blocks)
|
||||
get("/mutes", MastodonAPIController, :mutes)
|
||||
|
||||
get("/timelines/home", MastodonAPIController, :home_timeline)
|
||||
get("/timelines/home", MastodonAPIController, :home_timeline)
|
||||
get("/timelines/direct", MastodonAPIController, :dm_timeline)
|
||||
|
||||
get("/timelines/direct", MastodonAPIController, :dm_timeline)
|
||||
get("/favourites", MastodonAPIController, :favourites)
|
||||
get("/bookmarks", MastodonAPIController, :bookmarks)
|
||||
|
||||
get("/favourites", MastodonAPIController, :favourites)
|
||||
post("/notifications/clear", MastodonAPIController, :clear_notifications)
|
||||
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
|
||||
get("/notifications", MastodonAPIController, :notifications)
|
||||
get("/notifications/:id", MastodonAPIController, :get_notification)
|
||||
delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
|
||||
|
||||
post("/statuses", MastodonAPIController, :post_status)
|
||||
delete("/statuses/:id", MastodonAPIController, :delete_status)
|
||||
get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
|
||||
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
|
||||
|
||||
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
|
||||
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
|
||||
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
|
||||
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
|
||||
get("/lists", MastodonAPIController, :get_lists)
|
||||
get("/lists/:id", MastodonAPIController, :get_list)
|
||||
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
|
||||
|
||||
post("/notifications/clear", MastodonAPIController, :clear_notifications)
|
||||
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
|
||||
get("/notifications", MastodonAPIController, :notifications)
|
||||
get("/notifications/:id", MastodonAPIController, :get_notification)
|
||||
get("/domain_blocks", MastodonAPIController, :domain_blocks)
|
||||
|
||||
post("/media", MastodonAPIController, :upload)
|
||||
put("/media/:id", MastodonAPIController, :update_media)
|
||||
get("/filters", MastodonAPIController, :get_filters)
|
||||
|
||||
get("/lists", MastodonAPIController, :get_lists)
|
||||
get("/lists/:id", MastodonAPIController, :get_list)
|
||||
delete("/lists/:id", MastodonAPIController, :delete_list)
|
||||
post("/lists", MastodonAPIController, :create_list)
|
||||
put("/lists/:id", MastodonAPIController, :rename_list)
|
||||
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
|
||||
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
|
||||
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
|
||||
get("/suggestions", MastodonAPIController, :suggestions)
|
||||
|
||||
get("/domain_blocks", MastodonAPIController, :domain_blocks)
|
||||
post("/domain_blocks", MastodonAPIController, :block_domain)
|
||||
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
|
||||
get("/endorsements", MastodonAPIController, :empty_array)
|
||||
|
||||
get("/filters", MastodonAPIController, :get_filters)
|
||||
post("/filters", MastodonAPIController, :create_filter)
|
||||
get("/filters/:id", MastodonAPIController, :get_filter)
|
||||
put("/filters/:id", MastodonAPIController, :update_filter)
|
||||
delete("/filters/:id", MastodonAPIController, :delete_filter)
|
||||
get("/pleroma/flavour", MastodonAPIController, :get_flavour)
|
||||
end
|
||||
|
||||
get("/suggestions", MastodonAPIController, :suggestions)
|
||||
scope [] do
|
||||
pipe_through(:oauth_write)
|
||||
|
||||
get("/endorsements", MastodonAPIController, :empty_array)
|
||||
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
|
||||
|
||||
post("/statuses", MastodonAPIController, :post_status)
|
||||
delete("/statuses/:id", MastodonAPIController, :delete_status)
|
||||
|
||||
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
|
||||
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
|
||||
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
|
||||
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
|
||||
post("/statuses/:id/pin", MastodonAPIController, :pin_status)
|
||||
post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
|
||||
post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
|
||||
post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
|
||||
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
|
||||
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
|
||||
|
||||
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
|
||||
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
|
||||
|
||||
post("/media", MastodonAPIController, :upload)
|
||||
put("/media/:id", MastodonAPIController, :update_media)
|
||||
|
||||
delete("/lists/:id", MastodonAPIController, :delete_list)
|
||||
post("/lists", MastodonAPIController, :create_list)
|
||||
put("/lists/:id", MastodonAPIController, :rename_list)
|
||||
|
||||
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
|
||||
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
|
||||
|
||||
post("/filters", MastodonAPIController, :create_filter)
|
||||
get("/filters/:id", MastodonAPIController, :get_filter)
|
||||
put("/filters/:id", MastodonAPIController, :update_filter)
|
||||
delete("/filters/:id", MastodonAPIController, :delete_filter)
|
||||
|
||||
post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
|
||||
|
||||
post("/reports", MastodonAPIController, :reports)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_follow)
|
||||
|
||||
post("/follows", MastodonAPIController, :follow)
|
||||
post("/accounts/:id/follow", MastodonAPIController, :follow)
|
||||
|
||||
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
|
||||
post("/accounts/:id/block", MastodonAPIController, :block)
|
||||
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
|
||||
post("/accounts/:id/mute", MastodonAPIController, :mute)
|
||||
post("/accounts/:id/unmute", MastodonAPIController, :unmute)
|
||||
|
||||
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
|
||||
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
|
||||
|
||||
post("/domain_blocks", MastodonAPIController, :block_domain)
|
||||
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
|
||||
|
||||
post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
|
||||
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_push)
|
||||
|
||||
post("/push/subscription", SubscriptionController, :create)
|
||||
get("/push/subscription", SubscriptionController, :get)
|
||||
put("/push/subscription", SubscriptionController, :update)
|
||||
delete("/push/subscription", SubscriptionController, :delete)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api/web", Pleroma.Web.MastodonAPI do
|
||||
pipe_through(:authenticated_api)
|
||||
pipe_through([:authenticated_api, :oauth_write])
|
||||
|
||||
put("/settings", MastodonAPIController, :put_settings)
|
||||
end
|
||||
|
||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||
pipe_through(:api)
|
||||
|
||||
get("/instance", MastodonAPIController, :masto_instance)
|
||||
get("/instance/peers", MastodonAPIController, :peers)
|
||||
post("/apps", MastodonAPIController, :create_app)
|
||||
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
|
||||
get("/custom_emojis", MastodonAPIController, :custom_emojis)
|
||||
|
||||
get("/timelines/public", MastodonAPIController, :public_timeline)
|
||||
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
|
||||
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
|
||||
get("/statuses/:id/card", MastodonAPIController, :status_card)
|
||||
|
||||
get("/statuses/:id", MastodonAPIController, :get_status)
|
||||
get("/statuses/:id/context", MastodonAPIController, :get_context)
|
||||
get("/statuses/:id/card", MastodonAPIController, :empty_object)
|
||||
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
|
||||
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
|
||||
|
||||
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
|
||||
get("/accounts/:id/followers", MastodonAPIController, :followers)
|
||||
get("/accounts/:id/following", MastodonAPIController, :following)
|
||||
get("/accounts/:id", MastodonAPIController, :user)
|
||||
|
||||
get("/trends", MastodonAPIController, :empty_array)
|
||||
|
||||
get("/search", MastodonAPIController, :search)
|
||||
scope [] do
|
||||
pipe_through(:oauth_read_or_unauthenticated)
|
||||
|
||||
get("/timelines/public", MastodonAPIController, :public_timeline)
|
||||
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
|
||||
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
|
||||
|
||||
get("/statuses/:id", MastodonAPIController, :get_status)
|
||||
get("/statuses/:id/context", MastodonAPIController, :get_context)
|
||||
|
||||
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
|
||||
get("/accounts/:id/followers", MastodonAPIController, :followers)
|
||||
get("/accounts/:id/following", MastodonAPIController, :following)
|
||||
get("/accounts/:id", MastodonAPIController, :user)
|
||||
|
||||
get("/search", MastodonAPIController, :search)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api/v2", Pleroma.Web.MastodonAPI do
|
||||
pipe_through(:api)
|
||||
pipe_through([:api, :oauth_read_or_unauthenticated])
|
||||
get("/search", MastodonAPIController, :search2)
|
||||
end
|
||||
|
||||
|
|
@ -248,28 +408,44 @@ defmodule Pleroma.Web.Router do
|
|||
post("/help/test", TwitterAPI.UtilController, :help_test)
|
||||
get("/statusnet/config", TwitterAPI.UtilController, :config)
|
||||
get("/statusnet/version", TwitterAPI.UtilController, :version)
|
||||
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
|
||||
end
|
||||
|
||||
scope "/api", Pleroma.Web do
|
||||
pipe_through(:api)
|
||||
|
||||
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
|
||||
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
|
||||
get("/users/show", TwitterAPI.Controller, :show_user)
|
||||
|
||||
get("/statuses/followers", TwitterAPI.Controller, :followers)
|
||||
get("/statuses/friends", TwitterAPI.Controller, :friends)
|
||||
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
|
||||
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
|
||||
|
||||
post("/account/register", TwitterAPI.Controller, :register)
|
||||
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
|
||||
|
||||
get("/search", TwitterAPI.Controller, :search)
|
||||
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
|
||||
post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
|
||||
|
||||
get(
|
||||
"/account/confirm_email/:user_id/:token",
|
||||
TwitterAPI.Controller,
|
||||
:confirm_email,
|
||||
as: :confirm_email
|
||||
)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_read_or_unauthenticated)
|
||||
|
||||
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
|
||||
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
|
||||
get("/users/show", TwitterAPI.Controller, :show_user)
|
||||
|
||||
get("/statuses/followers", TwitterAPI.Controller, :followers)
|
||||
get("/statuses/friends", TwitterAPI.Controller, :friends)
|
||||
get("/statuses/blocks", TwitterAPI.Controller, :blocks)
|
||||
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
|
||||
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
|
||||
|
||||
get("/search", TwitterAPI.Controller, :search)
|
||||
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/api", Pleroma.Web do
|
||||
pipe_through(:api)
|
||||
pipe_through([:api, :oauth_read_or_unauthenticated])
|
||||
|
||||
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
|
||||
|
||||
|
|
@ -283,69 +459,92 @@ defmodule Pleroma.Web.Router do
|
|||
end
|
||||
|
||||
scope "/api", Pleroma.Web, as: :twitter_api_search do
|
||||
pipe_through(:api)
|
||||
pipe_through([:api, :oauth_read_or_unauthenticated])
|
||||
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
|
||||
end
|
||||
|
||||
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
|
||||
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
|
||||
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
|
||||
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
|
||||
|
||||
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
|
||||
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
|
||||
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
|
||||
scope [] do
|
||||
pipe_through(:oauth_read)
|
||||
|
||||
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
|
||||
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
|
||||
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
|
||||
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
|
||||
get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
|
||||
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
|
||||
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
|
||||
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
|
||||
|
||||
# XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
|
||||
# for now.
|
||||
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
|
||||
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
|
||||
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
|
||||
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
|
||||
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
|
||||
get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
|
||||
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
|
||||
|
||||
post("/statuses/update", TwitterAPI.Controller, :status_update)
|
||||
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
|
||||
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
|
||||
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
|
||||
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
|
||||
|
||||
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
|
||||
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
|
||||
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
|
||||
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
|
||||
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
|
||||
|
||||
post("/friendships/create", TwitterAPI.Controller, :follow)
|
||||
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
|
||||
post("/blocks/create", TwitterAPI.Controller, :block)
|
||||
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
|
||||
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
|
||||
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
|
||||
|
||||
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
|
||||
post("/media/upload", TwitterAPI.Controller, :upload_json)
|
||||
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
|
||||
|
||||
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
|
||||
post("/favorites/create", TwitterAPI.Controller, :favorite)
|
||||
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
|
||||
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
|
||||
end
|
||||
|
||||
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
|
||||
scope [] do
|
||||
pipe_through(:oauth_write)
|
||||
|
||||
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
|
||||
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
|
||||
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
|
||||
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
|
||||
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
|
||||
|
||||
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
|
||||
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
|
||||
post("/statuses/update", TwitterAPI.Controller, :status_update)
|
||||
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
|
||||
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
|
||||
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
|
||||
|
||||
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
|
||||
post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
|
||||
post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
|
||||
|
||||
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
|
||||
post("/media/upload", TwitterAPI.Controller, :upload_json)
|
||||
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
|
||||
|
||||
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
|
||||
post("/favorites/create", TwitterAPI.Controller, :favorite)
|
||||
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
|
||||
|
||||
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_follow)
|
||||
|
||||
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
|
||||
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
|
||||
|
||||
post("/friendships/create", TwitterAPI.Controller, :follow)
|
||||
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
|
||||
|
||||
post("/blocks/create", TwitterAPI.Controller, :block)
|
||||
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
|
||||
end
|
||||
end
|
||||
|
||||
pipeline :ap_relay do
|
||||
plug(:accepts, ["activity+json"])
|
||||
plug(:accepts, ["activity+json", "json"])
|
||||
end
|
||||
|
||||
pipeline :ostatus do
|
||||
plug(:accepts, ["xml", "atom", "html", "activity+json"])
|
||||
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
|
||||
end
|
||||
|
||||
pipeline :oembed do
|
||||
plug(:accepts, ["json", "xml"])
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
|
|
@ -354,6 +553,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/objects/:uuid", OStatus.OStatusController, :object)
|
||||
get("/activities/:uuid", OStatus.OStatusController, :activity)
|
||||
get("/notice/:id", OStatus.OStatusController, :notice)
|
||||
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
|
||||
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
|
||||
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
|
||||
|
||||
|
|
@ -363,8 +563,14 @@ defmodule Pleroma.Web.Router do
|
|||
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
pipe_through(:oembed)
|
||||
|
||||
get("/oembed", OEmbed.OEmbedController, :url)
|
||||
end
|
||||
|
||||
pipeline :activitypub do
|
||||
plug(:accepts, ["activity+json"])
|
||||
plug(:accepts, ["activity+json", "json"])
|
||||
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
|
||||
end
|
||||
|
||||
|
|
@ -375,6 +581,36 @@ defmodule Pleroma.Web.Router do
|
|||
get("/users/:nickname/followers", ActivityPubController, :followers)
|
||||
get("/users/:nickname/following", ActivityPubController, :following)
|
||||
get("/users/:nickname/outbox", ActivityPubController, :outbox)
|
||||
get("/objects/:uuid/likes", ActivityPubController, :object_likes)
|
||||
end
|
||||
|
||||
pipeline :activitypub_client do
|
||||
plug(:accepts, ["activity+json", "json"])
|
||||
plug(:fetch_session)
|
||||
plug(Pleroma.Plugs.OAuthPlug)
|
||||
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
|
||||
plug(Pleroma.Plugs.UserFetcherPlug)
|
||||
plug(Pleroma.Plugs.SessionAuthenticationPlug)
|
||||
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
|
||||
plug(Pleroma.Plugs.AuthenticationPlug)
|
||||
plug(Pleroma.Plugs.UserEnabledPlug)
|
||||
plug(Pleroma.Plugs.SetUserSessionIdPlug)
|
||||
plug(Pleroma.Plugs.EnsureUserKeyPlug)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
pipe_through([:activitypub_client])
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_read)
|
||||
get("/api/ap/whoami", ActivityPubController, :whoami)
|
||||
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_write)
|
||||
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
|
||||
end
|
||||
end
|
||||
|
||||
scope "/relay", Pleroma.Web.ActivityPub do
|
||||
|
|
@ -384,8 +620,8 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
scope "/", Pleroma.Web.ActivityPub do
|
||||
pipe_through(:activitypub)
|
||||
post("/users/:nickname/inbox", ActivityPubController, :inbox)
|
||||
post("/inbox", ActivityPubController, :inbox)
|
||||
post("/users/:nickname/inbox", ActivityPubController, :inbox)
|
||||
end
|
||||
|
||||
scope "/.well-known", Pleroma.Web do
|
||||
|
|
@ -404,9 +640,12 @@ defmodule Pleroma.Web.Router do
|
|||
pipe_through(:mastodon_html)
|
||||
|
||||
get("/web/login", MastodonAPIController, :login)
|
||||
post("/web/login", MastodonAPIController, :login_post)
|
||||
get("/web/*path", MastodonAPIController, :index)
|
||||
delete("/auth/sign_out", MastodonAPIController, :logout)
|
||||
|
||||
scope [] do
|
||||
pipe_through(:oauth_read_or_unauthenticated)
|
||||
get("/web/*path", MastodonAPIController, :index)
|
||||
end
|
||||
end
|
||||
|
||||
pipeline :remote_media do
|
||||
|
|
@ -414,12 +653,22 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
scope "/proxy/", Pleroma.Web.MediaProxy do
|
||||
pipe_through(:remote_media)
|
||||
|
||||
get("/:sig/:url", MediaProxyController, :remote)
|
||||
get("/:sig/:url/:filename", MediaProxyController, :remote)
|
||||
end
|
||||
|
||||
if Mix.env() == :dev do
|
||||
scope "/dev" do
|
||||
pipe_through([:mailbox_preview])
|
||||
|
||||
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", Fallback do
|
||||
get("/registration/:token", RedirectController, :registration_page)
|
||||
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
|
||||
get("/*path", RedirectController, :redirector)
|
||||
|
||||
options("/*path", RedirectController, :empty)
|
||||
|
|
@ -428,11 +677,36 @@ end
|
|||
|
||||
defmodule Fallback.RedirectController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Metadata
|
||||
|
||||
def redirector(conn, _params) do
|
||||
def redirector(conn, _params, code \\ 200) do
|
||||
conn
|
||||
|> put_resp_content_type("text/html")
|
||||
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.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 = Metadata.build_tags(params)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Salmon do
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
|
||||
use Bitwise
|
||||
alias Pleroma.Web.XML
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
require Logger
|
||||
|
||||
def decode(salmon) do
|
||||
|
|
@ -79,10 +86,10 @@ defmodule Pleroma.Web.Salmon do
|
|||
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
|
||||
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
|
||||
try do
|
||||
_ = :public_key.generate_key({:rsa, 2048, 65537})
|
||||
_ = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
|
||||
def generate_rsa_pem do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65537})
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
|
||||
{:ok, pem}
|
||||
|
|
@ -157,23 +164,31 @@ defmodule Pleroma.Web.Salmon do
|
|||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
|
||||
defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do
|
||||
with {:ok, %{status_code: code}} <-
|
||||
@doc "Pushes an activity to remote account."
|
||||
def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params),
|
||||
do: send_to_user(Map.put(params, :recipient, salmon))
|
||||
|
||||
def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do
|
||||
with {:ok, %{status: code}} when code in 200..299 <-
|
||||
poster.(
|
||||
salmon,
|
||||
url,
|
||||
feed,
|
||||
[{"Content-Type", "application/magic-envelope+xml"}],
|
||||
timeout: 10000,
|
||||
recv_timeout: 20000,
|
||||
hackney: [pool: :default]
|
||||
[{"Content-Type", "application/magic-envelope+xml"}]
|
||||
) do
|
||||
Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(url)
|
||||
|
||||
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
|
||||
:ok
|
||||
else
|
||||
e -> Logger.debug(fn -> "Pushing Salmon to #{salmon} failed, #{inspect(e)}" end)
|
||||
e ->
|
||||
unless params[:unreachable_since], do: Instances.set_reachable(url)
|
||||
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp send_to_user(_, _, _), do: nil
|
||||
def send_to_user(_), do: :noop
|
||||
|
||||
@supported_activities [
|
||||
"Create",
|
||||
|
|
@ -183,7 +198,12 @@ defmodule Pleroma.Web.Salmon do
|
|||
"Undo",
|
||||
"Delete"
|
||||
]
|
||||
def publish(user, activity, poster \\ &@httpoison.post/4)
|
||||
|
||||
@doc """
|
||||
Publishes an activity to remote accounts
|
||||
"""
|
||||
@spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none
|
||||
def publish(user, activity, poster \\ &@httpoison.post/3)
|
||||
|
||||
def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster)
|
||||
when type in @supported_activities do
|
||||
|
|
@ -198,12 +218,23 @@ defmodule Pleroma.Web.Salmon do
|
|||
{:ok, private, _} = keys_from_pem(keys)
|
||||
{:ok, feed} = encode(private, feed)
|
||||
|
||||
remote_users(activity)
|
||||
remote_users = remote_users(activity)
|
||||
|
||||
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
|
||||
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
|
||||
reachable_urls = Map.keys(reachable_urls_metadata)
|
||||
|
||||
remote_users
|
||||
|> Enum.filter(&(&1.info.salmon in reachable_urls))
|
||||
|> Enum.each(fn remote_user ->
|
||||
Task.start(fn ->
|
||||
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
|
||||
send_to_user(remote_user, feed, poster)
|
||||
end)
|
||||
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
|
||||
|
||||
Pleroma.Web.Federator.publish_single_salmon(%{
|
||||
recipient: remote_user,
|
||||
feed: feed,
|
||||
poster: poster,
|
||||
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Streamer do
|
||||
use GenServer
|
||||
require Logger
|
||||
alias Pleroma.{User, Notification, Activity, Object, Repo}
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
@keepalive_interval :timer.seconds(30)
|
||||
|
||||
def start_link do
|
||||
spawn(fn ->
|
||||
# 30 seconds
|
||||
Process.sleep(1000 * 30)
|
||||
GenServer.cast(__MODULE__, %{action: :ping})
|
||||
end)
|
||||
|
||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||
end
|
||||
|
||||
|
|
@ -30,6 +31,16 @@ defmodule Pleroma.Web.Streamer do
|
|||
GenServer.cast(__MODULE__, %{action: :stream, topic: topic, item: item})
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
spawn(fn ->
|
||||
# 30 seconds
|
||||
Process.sleep(@keepalive_interval)
|
||||
GenServer.cast(__MODULE__, %{action: :ping})
|
||||
end)
|
||||
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
def handle_cast(%{action: :ping}, topics) do
|
||||
Map.values(topics)
|
||||
|> List.flatten()
|
||||
|
|
@ -40,7 +51,7 @@ defmodule Pleroma.Web.Streamer do
|
|||
|
||||
spawn(fn ->
|
||||
# 30 seconds
|
||||
Process.sleep(1000 * 30)
|
||||
Process.sleep(@keepalive_interval)
|
||||
GenServer.cast(__MODULE__, %{action: :ping})
|
||||
end)
|
||||
|
||||
|
|
@ -61,20 +72,18 @@ defmodule Pleroma.Web.Streamer do
|
|||
end
|
||||
|
||||
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
|
||||
author = User.get_cached_by_ap_id(item.data["actor"])
|
||||
|
||||
# filter the recipient list if the activity is not public, see #270.
|
||||
recipient_lists =
|
||||
case ActivityPub.is_public?(item) do
|
||||
case Visibility.is_public?(item) do
|
||||
true ->
|
||||
Pleroma.List.get_lists_from_activity(item)
|
||||
|
||||
_ ->
|
||||
Pleroma.List.get_lists_from_activity(item)
|
||||
|> Enum.filter(fn list ->
|
||||
owner = Repo.get(User, list.user_id)
|
||||
owner = User.get_by_id(list.user_id)
|
||||
|
||||
ActivityPub.visible_for_user?(item, owner)
|
||||
Visibility.visible_for_user?(item, owner)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -98,10 +107,10 @@ defmodule Pleroma.Web.Streamer do
|
|||
%{
|
||||
event: "notification",
|
||||
payload:
|
||||
Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(
|
||||
socket.assigns["user"],
|
||||
item
|
||||
)
|
||||
NotificationView.render("show.json", %{
|
||||
notification: item,
|
||||
for: socket.assigns["user"]
|
||||
})
|
||||
|> Jason.encode!()
|
||||
}
|
||||
|> Jason.encode!()
|
||||
|
|
@ -189,10 +198,14 @@ defmodule Pleroma.Web.Streamer do
|
|||
if socket.assigns[:user] do
|
||||
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
|
||||
blocks = user.info.blocks || []
|
||||
mutes = user.info.mutes || []
|
||||
reblog_mutes = user.info.muted_reblogs || []
|
||||
|
||||
parent = Object.normalize(item.data["object"])
|
||||
parent = Object.normalize(item)
|
||||
|
||||
unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
|
||||
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
|
||||
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or
|
||||
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
|
||||
send(socket.transport_pid, {:text, represent_update(item, user)})
|
||||
end
|
||||
else
|
||||
|
|
@ -201,14 +214,29 @@ defmodule Pleroma.Web.Streamer do
|
|||
end)
|
||||
end
|
||||
|
||||
def push_to_socket(topics, topic, %Activity{
|
||||
data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
|
||||
}) do
|
||||
Enum.each(topics[topic] || [], fn socket ->
|
||||
send(
|
||||
socket.transport_pid,
|
||||
{:text, %{event: "delete", payload: to_string(deleted_activity_id)} |> Jason.encode!()}
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
def push_to_socket(_topics, _topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
|
||||
|
||||
def push_to_socket(topics, topic, item) do
|
||||
Enum.each(topics[topic] || [], fn socket ->
|
||||
# Get the current user so we have up-to-date blocks etc.
|
||||
if socket.assigns[:user] do
|
||||
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
|
||||
blocks = user.info.blocks || []
|
||||
mutes = user.info.mutes || []
|
||||
|
||||
unless item.actor in blocks do
|
||||
unless item.actor in blocks or item.actor in mutes or
|
||||
not ActivityPub.contain_activity(item, user) do
|
||||
send(socket.transport_pid, {:text, represent_update(item, user)})
|
||||
end
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,76 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
|
||||
<title>
|
||||
<%= Application.get_env(:pleroma, :instance)[:name] %>
|
||||
</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #282c37;
|
||||
background-color: #121a24;
|
||||
font-family: sans-serif;
|
||||
color:white;
|
||||
color: #b9b9ba;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 50px auto;
|
||||
max-width: 320px;
|
||||
padding: 0;
|
||||
padding: 40px 40px 40px 40px;
|
||||
background-color: #313543;
|
||||
max-width: 420px;
|
||||
padding: 20px;
|
||||
background-color: #182230;
|
||||
border-radius: 4px;
|
||||
margin: auto;
|
||||
margin-top: 10vh;
|
||||
box-shadow: 0 1px 4px 0px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #9baec8;
|
||||
color: #b9b9ba;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
margin-bottom: 40px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
text-align: left;
|
||||
color: #89898a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
box-sizing: content-box;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
background-color: rgba(0,0,0,.1);
|
||||
color: white;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #121a24;
|
||||
color: #b9b9ba;
|
||||
border: 0;
|
||||
border-bottom: 2px solid #9baec8;
|
||||
transition-property: border-bottom;
|
||||
transition-duration: 0.35s;
|
||||
border-bottom: 2px solid #2a384a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scopes-input {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
text-align: left;
|
||||
color: #89898a;
|
||||
}
|
||||
|
||||
.scopes-input label:first-child {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
|
||||
.scopes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
text-align: left;
|
||||
color: #b9b9ba;
|
||||
}
|
||||
|
||||
.scope {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
height: 2em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[type="checkbox"] + label:before {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-color: #121a24;
|
||||
border: 4px solid #121a24;
|
||||
box-sizing: border-box;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
margin-right: 1.0em;
|
||||
content: "";
|
||||
transition-property: background-color;
|
||||
transition-duration: 0.35s;
|
||||
color: #121a24;
|
||||
margin-bottom: -0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
[type="checkbox"]:checked + label:before {
|
||||
background-color: #d8a070;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-bottom: 2px solid #4b8ed8;
|
||||
outline: none;
|
||||
border-bottom: 2px solid #d8a070;
|
||||
}
|
||||
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: white;
|
||||
background-color: #419bdd;
|
||||
background-color: #1c2a3a;
|
||||
color: #b9b9ba;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 30px;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
box-shadow: 0px 0px 2px 0px black,
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 0px 1px #d8a070,
|
||||
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background-color: #931014;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #7d796a;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 440px) {
|
||||
.container {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
.scopes-input {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scope {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
}
|
||||
.form-row > label {
|
||||
text-align: left;
|
||||
line-height: 47px;
|
||||
flex: 1;
|
||||
}
|
||||
.form-row > input {
|
||||
flex: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Pleroma</h1>
|
||||
<h1><%= Application.get_env(:pleroma, :instance)[:name] %></h1>
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
16
lib/pleroma/web/templates/layout/metadata_player.html.eex
Normal file
16
lib/pleroma/web/templates/layout/metadata_player.html.eex
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<style type="text/css">
|
||||
video, audio {
|
||||
width:100%;
|
||||
max-width:600px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,23 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta content='width=device-width, initial-scale=1' name='viewport'>
|
||||
<title>
|
||||
<%= Application.get_env(:pleroma, :instance)[:name] %>
|
||||
</title>
|
||||
<meta charset='utf-8'>
|
||||
<meta content='width=device-width, initial-scale=1' name='viewport'>
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="stylesheet" media="all" href="/packs/common.css" />
|
||||
<link rel="stylesheet" media="all" href="/packs/default.css" />
|
||||
<script crossorigin='anonymous' src="/packs/locales.js"></script>
|
||||
<script crossorigin='anonymous' src="/packs/locales/<%= @flavour %>/en.js"></script>
|
||||
|
||||
<script src="/packs/common.js"></script>
|
||||
<script src="/packs/locale_en.js"></script>
|
||||
<link as='script' crossorigin='anonymous' href='/packs/features/getting_started.js' rel='preload'>
|
||||
<link as='script' crossorigin='anonymous' href='/packs/features/compose.js' rel='preload'>
|
||||
<link as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js' rel='preload'>
|
||||
<link as='script' crossorigin='anonymous' href='/packs/features/notifications.js' rel='preload'>
|
||||
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'>
|
||||
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
|
||||
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/home_timeline.js'>
|
||||
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/notifications.js'>
|
||||
<script id='initial-state' type='application/json'><%= raw @initial_state %></script>
|
||||
<script src="/packs/application.js"></script>
|
||||
|
||||
<script src="/packs/core/common.js"></script>
|
||||
<link rel="stylesheet" media="all" href="/packs/core/common.css" />
|
||||
|
||||
<script src="/packs/flavours/<%= @flavour %>/common.js"></script>
|
||||
<link rel="stylesheet" media="all" href="/packs/flavours/<%= @flavour %>/common.css" />
|
||||
|
||||
<script src="/packs/flavours/<%= @flavour %>/home.js"></script>
|
||||
</head>
|
||||
<body class='app-body no-reduce-motion system-font'>
|
||||
<div class='app-holder' data-props='{"locale":"en"}' id='mastodon'>
|
||||
|
|
|
|||
13
lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex
Normal file
13
lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div class="scopes-input">
|
||||
<%= label @form, :scope, "Permissions" %>
|
||||
|
||||
<div class="scopes">
|
||||
<%= for scope <- @available_scopes do %>
|
||||
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
|
||||
<div class="scope">
|
||||
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: assigns[:scope_param] || "scope[]" %>
|
||||
<%= label @form, :"scope_#{scope}", String.capitalize(scope) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
13
lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
Normal file
13
lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<h2>Sign in with external provider</h2>
|
||||
|
||||
<%= form_for @conn, o_auth_path(@conn, :prepare_request), [method: "get"], fn f -> %>
|
||||
<%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %>
|
||||
|
||||
<%= hidden_input f, :client_id, value: @client_id %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
|
||||
<%= for strategy <- Pleroma.Config.oauth_consumer_strategies() do %>
|
||||
<%= submit "Sign in with #{String.capitalize(strategy)}", name: "provider", value: strategy %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
43
lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
Normal file
43
lib/pleroma/web/templates/o_auth/o_auth/register.html.eex
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>Registration Details</h2>
|
||||
|
||||
<p>If you'd like to register a new account, please provide the details below.</p>
|
||||
|
||||
<%= form_for @conn, o_auth_path(@conn, :register), [], fn f -> %>
|
||||
|
||||
<div class="input">
|
||||
<%= label f, :nickname, "Nickname" %>
|
||||
<%= text_input f, :nickname, value: @nickname %>
|
||||
</div>
|
||||
<div class="input">
|
||||
<%= label f, :email, "Email" %>
|
||||
<%= text_input f, :email, value: @email %>
|
||||
</div>
|
||||
|
||||
<%= submit "Proceed as new user", name: "op", value: "register" %>
|
||||
|
||||
<p>Alternatively, sign in to connect to existing account.</p>
|
||||
|
||||
<div class="input">
|
||||
<%= label f, :auth_name, "Name or email" %>
|
||||
<%= text_input f, :auth_name %>
|
||||
</div>
|
||||
<div class="input">
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
</div>
|
||||
|
||||
<%= submit "Proceed as existing user", name: "op", value: "connect" %>
|
||||
|
||||
<%= hidden_input f, :client_id, value: @client_id %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :scope, value: Enum.join(@scopes, " ") %>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
|
||||
<% end %>
|
||||
|
|
@ -1,17 +1,31 @@
|
|||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
|
||||
<h2>OAuth Authorization</h2>
|
||||
|
||||
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
|
||||
<%= label f, :name, "Name or email" %>
|
||||
<%= text_input f, :name %>
|
||||
<br>
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
<br>
|
||||
<div class="input">
|
||||
<%= label f, :name, "Name or email" %>
|
||||
<%= text_input f, :name %>
|
||||
</div>
|
||||
<div class="input">
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
</div>
|
||||
|
||||
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f, scope_param: "authorization[scope][]"}) %>
|
||||
|
||||
<%= hidden_input f, :client_id, value: @client_id %>
|
||||
<%= hidden_input f, :response_type, value: @response_type %>
|
||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||
<%= hidden_input f, :scope, value: @scope %>
|
||||
<%= hidden_input f, :state, value: @state%>
|
||||
<%= hidden_input f, :state, value: @state %>
|
||||
<%= submit "Authorize" %>
|
||||
<% end %>
|
||||
|
||||
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
|
||||
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<h2>Password Reset for <%= @user.nickname %></h2>
|
||||
<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %>
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
<br>
|
||||
|
||||
<%= label f, :password_confirmation, "Confirmation" %>
|
||||
<%= password_input f, :password_confirmation %>
|
||||
<br>
|
||||
<%= hidden_input f, :token, value: @token.token %>
|
||||
<%= submit "Reset" %>
|
||||
<div class="form-row">
|
||||
<%= label f, :password, "Password" %>
|
||||
<%= password_input f, :password %>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<%= label f, :password_confirmation, "Confirmation" %>
|
||||
<%= password_input f, :password_confirmation %>
|
||||
</div>
|
||||
<%= hidden_input f, :token, value: @token.token %>
|
||||
<%= submit "Reset" %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
<h2>Password reset failed</h2>
|
||||
<h3><a href="<%= Pleroma.Web.base_url() %>">Homepage</a></h3>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue