Merge branch 'develop' into refactor/following-relationships
This commit is contained in:
commit
4ea1a61b00
219 changed files with 3097 additions and 4626 deletions
22
lib/mix/tasks/pleroma/count_statuses.ex
Normal file
22
lib/mix/tasks/pleroma/count_statuses.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
defmodule Mix.Tasks.Pleroma.CountStatuses do
|
||||
@shortdoc "Re-counts statuses for all users"
|
||||
|
||||
use Mix.Task
|
||||
alias Pleroma.User
|
||||
import Ecto.Query
|
||||
|
||||
def run([]) do
|
||||
Mix.Pleroma.start_pleroma()
|
||||
|
||||
stream =
|
||||
User
|
||||
|> where(local: true)
|
||||
|> Pleroma.Repo.stream()
|
||||
|
||||
Pleroma.Repo.transaction(fn ->
|
||||
Enum.each(stream, &User.update_note_count/1)
|
||||
end)
|
||||
|
||||
Mix.Pleroma.shell_info("Done")
|
||||
end
|
||||
end
|
||||
|
|
@ -28,7 +28,7 @@ defmodule Mix.Tasks.Pleroma.Database do
|
|||
Logger.info("Removing embedded objects")
|
||||
|
||||
Repo.query!(
|
||||
"update activities set data = jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
|
||||
"update activities set data = safe_jsonb_set(data, '{object}'::text[], data->'object'->'id') where data->'object'->>'id' is not null;",
|
||||
[],
|
||||
timeout: :infinity
|
||||
)
|
||||
|
|
@ -126,7 +126,7 @@ defmodule Mix.Tasks.Pleroma.Database do
|
|||
set: [
|
||||
data:
|
||||
fragment(
|
||||
"jsonb_set(?, '{likes}', '[]'::jsonb, true)",
|
||||
"safe_jsonb_set(?, '{likes}', '[]'::jsonb, true)",
|
||||
object.data
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -111,19 +111,21 @@ defmodule Mix.Tasks.Pleroma.Emoji do
|
|||
file_list: files_to_unzip
|
||||
)
|
||||
|
||||
IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
|
||||
IO.puts(IO.ANSI.format(["Writing pack.json for ", :bright, pack_name]))
|
||||
|
||||
emoji_txt_str =
|
||||
Enum.map(
|
||||
files,
|
||||
fn {shortcode, path} ->
|
||||
emojo_path = Path.join("/emoji/#{pack_name}", path)
|
||||
"#{shortcode}, #{emojo_path}"
|
||||
end
|
||||
)
|
||||
|> Enum.join("\n")
|
||||
pack_json = %{
|
||||
pack: %{
|
||||
"license" => pack["license"],
|
||||
"homepage" => pack["homepage"],
|
||||
"description" => pack["description"],
|
||||
"fallback-src" => pack["src"],
|
||||
"fallback-src-sha256" => pack["src_sha256"],
|
||||
"share-files" => true
|
||||
},
|
||||
files: files
|
||||
}
|
||||
|
||||
File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
|
||||
File.write!(Path.join(pack_path, "pack.json"), Jason.encode!(pack_json, pretty: true))
|
||||
else
|
||||
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
defmodule Mix.Tasks.Pleroma.Relay do
|
||||
use Mix.Task
|
||||
import Mix.Pleroma
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
||||
@shortdoc "Manages remote relays"
|
||||
|
|
@ -36,14 +35,10 @@ defmodule Mix.Tasks.Pleroma.Relay do
|
|||
def run(["list"]) do
|
||||
start_pleroma()
|
||||
|
||||
with %User{} = user <- Relay.get_actor() do
|
||||
user
|
||||
|> User.following()
|
||||
|> Enum.map(fn entry -> URI.parse(entry).host end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(&shell_info(&1))
|
||||
with {:ok, list} <- Relay.list() do
|
||||
list |> Enum.each(&shell_info(&1))
|
||||
else
|
||||
e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
|
||||
{:error, e} -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -161,11 +161,6 @@ defmodule Pleroma.Application do
|
|||
id: :web_push_init,
|
||||
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
||||
restart: :temporary
|
||||
},
|
||||
%{
|
||||
id: :federator_init,
|
||||
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
|
||||
restart: :temporary
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
@ -177,11 +172,6 @@ defmodule Pleroma.Application do
|
|||
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
|
||||
restart: :temporary
|
||||
},
|
||||
%{
|
||||
id: :federator_init,
|
||||
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
|
||||
restart: :temporary
|
||||
},
|
||||
%{
|
||||
id: :internal_fetch_init,
|
||||
start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ defmodule Pleroma.Conversation.Participation do
|
|||
|> validate_required([:read])
|
||||
end
|
||||
|
||||
def mark_as_read(%User{} = user, %Conversation{} = conversation) do
|
||||
with %__MODULE__{} = participation <- for_user_and_conversation(user, conversation) do
|
||||
mark_as_read(participation)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_read(participation) do
|
||||
participation
|
||||
|> read_cng(%{read: true})
|
||||
|
|
@ -63,6 +69,19 @@ defmodule Pleroma.Conversation.Participation do
|
|||
end
|
||||
end
|
||||
|
||||
def mark_all_as_read(user) do
|
||||
{_, participations} =
|
||||
__MODULE__
|
||||
|> where([p], p.user_id == ^user.id)
|
||||
|> where([p], not p.read)
|
||||
|> update([p], set: [read: true])
|
||||
|> select([p], p)
|
||||
|> Repo.update_all([])
|
||||
|
||||
User.set_unread_conversation_count(user)
|
||||
{:ok, participations}
|
||||
end
|
||||
|
||||
def mark_as_unread(participation) do
|
||||
participation
|
||||
|> read_cng(%{read: false})
|
||||
|
|
|
|||
|
|
@ -86,18 +86,18 @@ defmodule Pleroma.ModerationLog do
|
|||
parsed_datetime
|
||||
end
|
||||
|
||||
@spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
|
||||
@spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
|
||||
{:ok, ModerationLog} | {:error, any}
|
||||
def insert_log(%{
|
||||
actor: %User{} = actor,
|
||||
subject: %User{} = subject,
|
||||
subject: subjects,
|
||||
action: action,
|
||||
permission: permission
|
||||
}) do
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
"actor" => user_to_map(actor),
|
||||
"subject" => user_to_map(subject),
|
||||
"subject" => user_to_map(subjects),
|
||||
"action" => action,
|
||||
"permission" => permission,
|
||||
"message" => ""
|
||||
|
|
@ -303,13 +303,16 @@ defmodule Pleroma.ModerationLog do
|
|||
end
|
||||
|
||||
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
||||
|
||||
defp insert_log_entry_with_message(entry) do
|
||||
entry.data["message"]
|
||||
|> put_in(get_log_entry_message(entry))
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp user_to_map(users) when is_list(users) do
|
||||
users |> Enum.map(&user_to_map/1)
|
||||
end
|
||||
|
||||
defp user_to_map(%User{} = user) do
|
||||
user
|
||||
|> Map.from_struct()
|
||||
|
|
@ -349,10 +352,10 @@ defmodule Pleroma.ModerationLog do
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "delete",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => subjects
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} deleted user @#{subject_nickname}"
|
||||
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -363,12 +366,7 @@ defmodule Pleroma.ModerationLog do
|
|||
"subjects" => subjects
|
||||
}
|
||||
}) do
|
||||
nicknames =
|
||||
subjects
|
||||
|> Enum.map(&"@#{&1["nickname"]}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} created users: #{nicknames}"
|
||||
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -376,10 +374,10 @@ defmodule Pleroma.ModerationLog do
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "activate",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => users
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} activated user @#{subject_nickname}"
|
||||
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -387,10 +385,10 @@ defmodule Pleroma.ModerationLog do
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "deactivate",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => users
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} deactivated user @#{subject_nickname}"
|
||||
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -402,14 +400,9 @@ defmodule Pleroma.ModerationLog do
|
|||
"action" => "tag"
|
||||
}
|
||||
}) do
|
||||
nicknames_string =
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
tags_string = tags |> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
|
||||
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -421,14 +414,9 @@ defmodule Pleroma.ModerationLog do
|
|||
"action" => "untag"
|
||||
}
|
||||
}) do
|
||||
nicknames_string =
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
tags_string = tags |> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
|
||||
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -436,11 +424,11 @@ defmodule Pleroma.ModerationLog do
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "grant",
|
||||
"subject" => %{"nickname" => subject_nickname},
|
||||
"subject" => users,
|
||||
"permission" => permission
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} made @#{subject_nickname} #{permission}"
|
||||
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -448,11 +436,11 @@ defmodule Pleroma.ModerationLog do
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "revoke",
|
||||
"subject" => %{"nickname" => subject_nickname},
|
||||
"subject" => users,
|
||||
"permission" => permission
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
|
||||
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
|
@ -551,4 +539,16 @@ defmodule Pleroma.ModerationLog do
|
|||
}) do
|
||||
"@#{actor_nickname} deleted status ##{subject_id}"
|
||||
end
|
||||
|
||||
defp nicknames_to_string(nicknames) do
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
|
||||
defp users_to_nicknames_string(users) do
|
||||
users
|
||||
|> Enum.map(&"@#{&1["nickname"]}")
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Notification do
|
|||
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
require Logger
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
|
|
@ -34,43 +35,92 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
|
||||
def for_user_query(user, opts \\ []) do
|
||||
query =
|
||||
Notification
|
||||
|> where(user_id: ^user.id)
|
||||
Notification
|
||||
|> where(user_id: ^user.id)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment(
|
||||
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
||||
a.actor
|
||||
)
|
||||
)
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|> join(:left, [n, a], object in Object,
|
||||
on:
|
||||
fragment(
|
||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
||||
object.data,
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|> exclude_muted(user, opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
defp exclude_muted(query, _, %{with_muted: true}) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_muted(query, user, _opts) do
|
||||
query
|
||||
|> where([n, a], a.actor not in ^user.info.muted_notifications)
|
||||
|> where([n, a], a.actor not in ^user.info.blocks)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
||||
)
|
||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
query
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment(
|
||||
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
||||
a.actor
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|> join(:left, [n, a], object in Object,
|
||||
on:
|
||||
fragment(
|
||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
||||
object.data,
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|
||||
if opts[:with_muted] do
|
||||
query
|
||||
else
|
||||
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
|
||||
|> where([n, a], a.actor not in ^user.info.blocks)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
||||
)
|
||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
query
|
||||
|> where(
|
||||
[n, a],
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility not in @valid_visibilities do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, _visibility), do: query
|
||||
|
||||
def for_user(user, opts \\ %{}) do
|
||||
user
|
||||
|> for_user_query(opts)
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ defmodule Pleroma.Object do
|
|||
data:
|
||||
fragment(
|
||||
"""
|
||||
jsonb_set(?, '{repliesCount}',
|
||||
safe_jsonb_set(?, '{repliesCount}',
|
||||
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
|
||||
""",
|
||||
o.data,
|
||||
|
|
@ -204,7 +204,7 @@ defmodule Pleroma.Object do
|
|||
data:
|
||||
fragment(
|
||||
"""
|
||||
jsonb_set(?, '{repliesCount}',
|
||||
safe_jsonb_set(?, '{repliesCount}',
|
||||
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
|
||||
""",
|
||||
o.data,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,23 @@ defmodule Pleroma.Object.Containment do
|
|||
get_actor(%{"actor" => actor})
|
||||
end
|
||||
|
||||
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
|
||||
# objects being present in the test suite environment. Once these objects are
|
||||
# removed, please also remove this.
|
||||
if Mix.env() == :test do
|
||||
defp compare_uris(_, %URI{scheme: "tag"}), do: :ok
|
||||
end
|
||||
|
||||
defp compare_uris(%URI{} = id_uri, %URI{} = other_uri) do
|
||||
if id_uri.host == other_uri.host do
|
||||
:ok
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp compare_uris(_, _), do: :error
|
||||
|
||||
@doc """
|
||||
Checks that an imported AP object's actor matches the domain it came from.
|
||||
"""
|
||||
|
|
@ -41,11 +58,7 @@ defmodule Pleroma.Object.Containment do
|
|||
id_uri = URI.parse(id)
|
||||
actor_uri = URI.parse(get_actor(params))
|
||||
|
||||
if id_uri.host == actor_uri.host do
|
||||
:ok
|
||||
else
|
||||
:error
|
||||
end
|
||||
compare_uris(actor_uri, id_uri)
|
||||
end
|
||||
|
||||
def contain_origin(id, %{"attributedTo" => actor} = params),
|
||||
|
|
@ -57,11 +70,7 @@ defmodule Pleroma.Object.Containment do
|
|||
id_uri = URI.parse(id)
|
||||
other_uri = URI.parse(other_id)
|
||||
|
||||
if id_uri.host == other_uri.host do
|
||||
:ok
|
||||
else
|
||||
:error
|
||||
end
|
||||
compare_uris(id_uri, other_uri)
|
||||
end
|
||||
|
||||
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ defmodule Pleroma.Object.Fetcher do
|
|||
alias Pleroma.Signature
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.OStatus
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
|
@ -67,7 +66,8 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
|
||||
params <- prepare_activity_params(data),
|
||||
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
|
||||
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
|
||||
{:transmogrifier, {:ok, activity}} <-
|
||||
{:transmogrifier, Transmogrifier.handle_incoming(params, options)},
|
||||
{:object, _data, %Object{} = object} <-
|
||||
{:object, data, Object.normalize(activity, false)} do
|
||||
{:ok, object}
|
||||
|
|
@ -75,9 +75,12 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:containment, _} ->
|
||||
{:error, "Object containment failed."}
|
||||
|
||||
{:error, {:reject, nil}} ->
|
||||
{:transmogrifier, {:error, {:reject, nil}}} ->
|
||||
{:reject, nil}
|
||||
|
||||
{:transmogrifier, _} ->
|
||||
{:error, "Transmogrifier failure."}
|
||||
|
||||
{:object, data, nil} ->
|
||||
reinject_object(%Object{}, data)
|
||||
|
||||
|
|
@ -87,15 +90,8 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:fetch_object, %Object{} = object} ->
|
||||
{:ok, object}
|
||||
|
||||
_e ->
|
||||
# Only fallback when receiving a fetch/normalization error with ActivityPub
|
||||
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
|
||||
|
||||
# FIXME: OStatus Object Containment?
|
||||
case OStatus.fetch_activity_from_url(id) do
|
||||
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
|
||||
e -> e
|
||||
end
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +110,8 @@ defmodule Pleroma.Object.Fetcher do
|
|||
with {:ok, object} <- fetch_object_from_id(id, options) do
|
||||
object
|
||||
else
|
||||
_e ->
|
||||
e ->
|
||||
Logger.error("Error while fetching #{id}: #{inspect(e)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
@ -161,7 +158,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
|
||||
Logger.debug("Fetch headers: #{inspect(headers)}")
|
||||
|
||||
with true <- String.starts_with?(id, "http"),
|
||||
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers),
|
||||
{:ok, data} <- Jason.decode(body),
|
||||
:ok <- Containment.contain_origin_from_id(id, data) do
|
||||
|
|
@ -170,6 +167,9 @@ defmodule Pleroma.Object.Fetcher do
|
|||
{:ok, %{status: code}} when code in [404, 410] ->
|
||||
{:error, "Object has been deleted"}
|
||||
|
||||
{:scheme, _} ->
|
||||
{:error, "Unsupported URI scheme"}
|
||||
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -401,11 +401,9 @@ defmodule Pleroma.ReverseProxy do
|
|||
|
||||
defp client, do: Pleroma.ReverseProxy.Client
|
||||
|
||||
defp track_failed_url(url, code, opts) do
|
||||
code = to_string(code)
|
||||
|
||||
defp track_failed_url(url, error, opts) do
|
||||
ttl =
|
||||
if code in ["403", "404"] or String.starts_with?(code, "5") do
|
||||
unless error in [:body_too_large, 400, 204] do
|
||||
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
|
||||
else
|
||||
nil
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ defmodule Pleroma.Upload do
|
|||
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
|
||||
end
|
||||
|
||||
opts = %{
|
||||
%{
|
||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
|
||||
|
|
@ -118,37 +118,6 @@ defmodule Pleroma.Upload do
|
|||
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
|
||||
)
|
||||
}
|
||||
|
||||
# TODO: 1.0+ : remove old config compatibility
|
||||
opts =
|
||||
if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
|
||||
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
|
||||
Logger.warn("""
|
||||
Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
|
||||
|
||||
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
|
||||
|
||||
:pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"]
|
||||
""")
|
||||
|
||||
Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"])
|
||||
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
|
||||
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
|
||||
Logger.warn("""
|
||||
Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
|
||||
|
||||
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
|
||||
""")
|
||||
|
||||
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
|
||||
else
|
||||
opts
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
|
||||
alias Pleroma.Web.OAuth
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.RelMe
|
||||
alias Pleroma.Web.Websub
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
||||
require Logger
|
||||
|
|
@ -418,10 +416,6 @@ defmodule Pleroma.User do
|
|||
{:error, "Could not follow user: #{followed.nickname} blocked you."}
|
||||
|
||||
true ->
|
||||
if !followed.local && follower.local && !ap_enabled?(followed) do
|
||||
Websub.subscribe(follower, followed)
|
||||
end
|
||||
|
||||
FollowingRelationship.follow(follower, followed, state)
|
||||
|
||||
follower = maybe_update_following_count(follower)
|
||||
|
|
@ -576,12 +570,7 @@ defmodule Pleroma.User do
|
|||
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
|
||||
end
|
||||
|
||||
def fetch_by_nickname(nickname) do
|
||||
case ActivityPub.make_user_from_nickname(nickname) do
|
||||
{:ok, user} -> {:ok, user}
|
||||
_ -> OStatus.make_user(nickname)
|
||||
end
|
||||
end
|
||||
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
|
||||
|
||||
def get_or_fetch_by_nickname(nickname) do
|
||||
with %User{} = user <- get_by_nickname(nickname) do
|
||||
|
|
@ -678,7 +667,7 @@ defmodule Pleroma.User do
|
|||
set: [
|
||||
info:
|
||||
fragment(
|
||||
"jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
|
||||
"safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
|
||||
u.info,
|
||||
u.info
|
||||
)
|
||||
|
|
@ -699,7 +688,7 @@ defmodule Pleroma.User do
|
|||
set: [
|
||||
info:
|
||||
fragment(
|
||||
"jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
|
||||
"safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
|
||||
u.info,
|
||||
u.info
|
||||
)
|
||||
|
|
@ -769,7 +758,7 @@ defmodule Pleroma.User do
|
|||
set: [
|
||||
info:
|
||||
fragment(
|
||||
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
|
||||
"safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
|
||||
u.info,
|
||||
s.count
|
||||
)
|
||||
|
|
@ -1000,7 +989,15 @@ defmodule Pleroma.User do
|
|||
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
|
||||
end
|
||||
|
||||
def deactivate(%User{} = user, status \\ true) do
|
||||
def deactivate(user, status \\ true)
|
||||
|
||||
def deactivate(users, status) when is_list(users) do
|
||||
Repo.transaction(fn ->
|
||||
for user <- users, do: deactivate(user, status)
|
||||
end)
|
||||
end
|
||||
|
||||
def deactivate(%User{} = user, status) do
|
||||
with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
|
||||
Enum.each(get_followers(user), &invalidate_cache/1)
|
||||
Enum.each(get_friends(user), &update_follower_count/1)
|
||||
|
|
@ -1013,6 +1010,10 @@ defmodule Pleroma.User do
|
|||
update_info(user, &User.Info.update_notification_settings(&1, settings))
|
||||
end
|
||||
|
||||
def delete(users) when is_list(users) do
|
||||
for user <- users, do: delete(user)
|
||||
end
|
||||
|
||||
def delete(%User{} = user) do
|
||||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||
end
|
||||
|
|
@ -1175,18 +1176,7 @@ defmodule Pleroma.User do
|
|||
|
||||
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
|
||||
|
||||
def fetch_by_ap_id(ap_id) do
|
||||
case ActivityPub.make_user_from_ap_id(ap_id) do
|
||||
{:ok, user} ->
|
||||
{:ok, user}
|
||||
|
||||
_ ->
|
||||
case OStatus.make_user(ap_id) do
|
||||
{:ok, user} -> {:ok, user}
|
||||
_ -> {:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
end
|
||||
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
|
||||
|
||||
def get_or_fetch_by_ap_id(ap_id) do
|
||||
user = get_cached_by_ap_id(ap_id)
|
||||
|
|
@ -1241,11 +1231,6 @@ defmodule Pleroma.User do
|
|||
{:ok, key}
|
||||
end
|
||||
|
||||
# OStatus Magic Key
|
||||
def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
|
||||
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
|
||||
end
|
||||
|
||||
def public_key_from_info(_), do: {:error, "not found key"}
|
||||
|
||||
def get_public_key_for_ap_id(ap_id) do
|
||||
|
|
@ -1566,6 +1551,12 @@ defmodule Pleroma.User do
|
|||
|
||||
`fun` is called with the `user.info`.
|
||||
"""
|
||||
def update_info(users, fun) when is_list(users) do
|
||||
Repo.transaction(fn ->
|
||||
for user <- users, do: update_info(user, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
def update_info(user, fun) do
|
||||
user
|
||||
|> change_info(fun)
|
||||
|
|
|
|||
|
|
@ -39,9 +39,6 @@ defmodule Pleroma.User.Info do
|
|||
field(:settings, :map, default: nil)
|
||||
field(:magic_key, :string, default: nil)
|
||||
field(:uri, :string, default: nil)
|
||||
field(:topic, :string, default: nil)
|
||||
field(:hub, :string, default: nil)
|
||||
field(:salmon, :string, default: nil)
|
||||
field(:hide_followers_count, :boolean, default: false)
|
||||
field(:hide_follows_count, :boolean, default: false)
|
||||
field(:hide_followers, :boolean, default: false)
|
||||
|
|
@ -262,9 +259,6 @@ defmodule Pleroma.User.Info do
|
|||
:locked,
|
||||
:magic_key,
|
||||
:uri,
|
||||
:hub,
|
||||
:topic,
|
||||
:salmon,
|
||||
:hide_followers,
|
||||
:hide_follows,
|
||||
:hide_followers_count,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
defmodule Pleroma.User.Search do
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
import Ecto.Query
|
||||
|
||||
@similarity_threshold 0.25
|
||||
@limit 20
|
||||
|
||||
def search(query_string, opts \\ []) do
|
||||
|
|
@ -23,18 +21,10 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
maybe_resolve(resolve, for_user, query_string)
|
||||
|
||||
{:ok, results} =
|
||||
Repo.transaction(fn ->
|
||||
Ecto.Adapters.SQL.query(
|
||||
Repo,
|
||||
"select set_limit(#{@similarity_threshold})",
|
||||
[]
|
||||
)
|
||||
|
||||
query_string
|
||||
|> search_query(for_user, following)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
end)
|
||||
results =
|
||||
query_string
|
||||
|> search_query(for_user, following)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
|
||||
results
|
||||
end
|
||||
|
|
@ -56,15 +46,65 @@ defmodule Pleroma.User.Search do
|
|||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> search_subqueries(query_string)
|
||||
|> union_subqueries
|
||||
|> distinct_query()
|
||||
|> boost_search_rank_query(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> trigram_rank(query_string)
|
||||
|> boost_search_rank(for_user)
|
||||
|> subquery()
|
||||
|> order_by(desc: :search_rank)
|
||||
|> maybe_restrict_local(for_user)
|
||||
end
|
||||
|
||||
@nickname_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~\-@]+$/
|
||||
defp fts_search(query, query_string) do
|
||||
{nickname_weight, name_weight} =
|
||||
if String.match?(query_string, @nickname_regex) do
|
||||
{"A", "B"}
|
||||
else
|
||||
{"B", "A"}
|
||||
end
|
||||
|
||||
query_string = to_tsquery(query_string)
|
||||
|
||||
from(
|
||||
u in query,
|
||||
where:
|
||||
fragment(
|
||||
"""
|
||||
(setweight(to_tsvector('simple', ?), ?) || setweight(to_tsvector('simple', ?), ?)) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.name,
|
||||
^name_weight,
|
||||
u.nickname,
|
||||
^nickname_weight,
|
||||
^query_string
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp to_tsquery(query_string) do
|
||||
String.trim_trailing(query_string, "@" <> local_domain())
|
||||
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|
||||
|> String.trim()
|
||||
|> String.split()
|
||||
|> Enum.map(&(&1 <> ":*"))
|
||||
|> Enum.join(" | ")
|
||||
end
|
||||
|
||||
defp trigram_rank(query, query_string) do
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
^query_string,
|
||||
u.nickname,
|
||||
u.name
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp base_query(_user, false), do: User
|
||||
defp base_query(user, true), do: User.get_followers_query(user)
|
||||
|
||||
|
|
@ -87,21 +127,6 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
defp filter_blocked_domains(query, _), do: query
|
||||
|
||||
defp union_subqueries({fts_subquery, trigram_subquery}) do
|
||||
from(s in trigram_subquery, union_all: ^fts_subquery)
|
||||
end
|
||||
|
||||
defp search_subqueries(base_query, query_string) do
|
||||
{
|
||||
fts_search_subquery(base_query, query_string),
|
||||
trigram_search_subquery(base_query, query_string)
|
||||
}
|
||||
end
|
||||
|
||||
defp distinct_query(q) do
|
||||
from(s in subquery(q), order_by: s.search_type, distinct: s.id)
|
||||
end
|
||||
|
||||
defp maybe_resolve(true, user, query) do
|
||||
case {limit(), user} do
|
||||
{:all, _} -> :noop
|
||||
|
|
@ -126,9 +151,9 @@ defmodule Pleroma.User.Search do
|
|||
|
||||
defp restrict_local(q), do: where(q, [u], u.local == true)
|
||||
|
||||
defp boost_search_rank_query(query, nil), do: query
|
||||
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||
|
||||
defp boost_search_rank_query(query, for_user) do
|
||||
defp boost_search_rank(query, %User{} = for_user) do
|
||||
friends_ids = User.get_friends_ids(for_user)
|
||||
followers_ids = User.get_followers_ids(for_user)
|
||||
|
||||
|
|
@ -137,8 +162,8 @@ defmodule Pleroma.User.Search do
|
|||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
CASE WHEN (?) THEN 0.5 + (?) * 1.3
|
||||
WHEN (?) THEN 0.5 + (?) * 1.2
|
||||
CASE WHEN (?) THEN (?) * 1.5
|
||||
WHEN (?) THEN (?) * 1.3
|
||||
WHEN (?) THEN (?) * 1.1
|
||||
ELSE (?) END
|
||||
""",
|
||||
|
|
@ -154,70 +179,5 @@ defmodule Pleroma.User.Search do
|
|||
)
|
||||
end
|
||||
|
||||
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
|
||||
defp fts_search_subquery(query, term) do
|
||||
processed_query =
|
||||
String.trim_trailing(term, "@" <> local_domain())
|
||||
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|
||||
|> String.trim()
|
||||
|> String.split()
|
||||
|> Enum.map(&(&1 <> ":*"))
|
||||
|> Enum.join(" | ")
|
||||
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_type: ^0,
|
||||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
ts_rank_cd(
|
||||
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
|
||||
to_tsquery('simple', ?),
|
||||
32
|
||||
)
|
||||
""",
|
||||
u.nickname,
|
||||
u.name,
|
||||
^processed_query
|
||||
)
|
||||
},
|
||||
where:
|
||||
fragment(
|
||||
"""
|
||||
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.nickname,
|
||||
u.name,
|
||||
^processed_query
|
||||
)
|
||||
)
|
||||
|> User.restrict_deactivated()
|
||||
end
|
||||
|
||||
@spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
|
||||
defp trigram_search_subquery(query, term) do
|
||||
term = String.trim_trailing(term, "@" <> local_domain())
|
||||
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
|
||||
search_type: fragment("?", 1),
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
^term,
|
||||
u.nickname,
|
||||
u.name
|
||||
)
|
||||
},
|
||||
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
|
||||
)
|
||||
|> User.restrict_deactivated()
|
||||
end
|
||||
|
||||
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||
defp boost_search_rank(query, _for_user), do: query
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
alias Pleroma.Activity.Ir.Topics
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
|
|
@ -131,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
{:ok, map} <- MRF.filter(map),
|
||||
{recipients, _, _} = get_recipients(map),
|
||||
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
|
||||
:ok <- Containment.contain_child(map),
|
||||
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
|
||||
{:ok, map, object} <- insert_full_object(map) do
|
||||
{:ok, activity} =
|
||||
Repo.insert(%Activity{
|
||||
|
|
@ -153,11 +154,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
Notification.create_notifications(activity)
|
||||
|
||||
participations =
|
||||
activity
|
||||
|> Conversation.create_or_bump_for()
|
||||
|> get_participations()
|
||||
|
||||
conversation = create_or_bump_conversation(activity, map["actor"])
|
||||
participations = get_participations(conversation)
|
||||
stream_out(activity)
|
||||
stream_out_participations(participations)
|
||||
{:ok, activity}
|
||||
|
|
@ -182,7 +180,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
defp get_participations({:ok, %{participations: participations}}), do: participations
|
||||
defp create_or_bump_conversation(activity, actor) do
|
||||
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor),
|
||||
Participation.mark_as_read(user, conversation) do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_participations({:ok, conversation}) do
|
||||
conversation
|
||||
|> Repo.preload(:participations, force: true)
|
||||
|> Map.get(:participations)
|
||||
end
|
||||
|
||||
defp get_participations(_), do: []
|
||||
|
||||
def stream_out_participations(participations) do
|
||||
|
|
@ -225,6 +236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
published = params[:published]
|
||||
quick_insert? = Pleroma.Config.get([:env]) == :benchmark
|
||||
|
||||
with create_data <-
|
||||
make_create_data(
|
||||
|
|
@ -235,12 +247,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
{:fake, false, activity} <- {:fake, fake, activity},
|
||||
_ <- increase_replies_count_if_reply(create_data),
|
||||
_ <- increase_poll_votes_if_vote(create_data),
|
||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||
# 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
|
||||
{:quick_insert, true, activity} ->
|
||||
{:ok, activity}
|
||||
|
||||
{:fake, true, activity} ->
|
||||
{:ok, activity}
|
||||
|
||||
|
|
@ -269,22 +285,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def accept(%{to: to, actor: actor, object: object} = params) do
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
|
||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
def accept(params) do
|
||||
accept_or_reject("Accept", params)
|
||||
end
|
||||
|
||||
def reject(%{to: to, actor: actor, object: object} = params) do
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
def reject(params) do
|
||||
accept_or_reject("Reject", params)
|
||||
end
|
||||
|
||||
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
|
||||
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||
local = Map.get(params, :local, true)
|
||||
activity_id = Map.get(params, :activity_id, nil)
|
||||
|
||||
with data <-
|
||||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
||||
|> Utils.maybe_put("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
|
@ -409,18 +424,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
|
||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do
|
||||
local = Keyword.get(options, :local, true)
|
||||
activity_id = Keyword.get(options, :activity_id, nil)
|
||||
actor = Keyword.get(options, :actor, actor)
|
||||
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||
|
||||
with {:ok, object, activity} <- Object.delete(object),
|
||||
data <- %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
},
|
||||
data <-
|
||||
%{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
}
|
||||
|> maybe_put("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local, false),
|
||||
stream_out_participations(object, user),
|
||||
_ <- decrease_replies_count_if_reply(object),
|
||||
|
|
@ -593,6 +614,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_visibility(query, _visibility), do: query
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
else
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ?",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when visibility not in @valid_visibilities do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, _visibility), do: query
|
||||
|
||||
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
|
||||
do: query
|
||||
|
||||
|
|
@ -957,6 +1021,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> restrict_muted_reblogs(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
||||
|
|
@ -1156,7 +1221,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
data <- maybe_update_follow_information(data) do
|
||||
{:ok, data}
|
||||
else
|
||||
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
e ->
|
||||
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
[]
|
||||
end
|
||||
|
||||
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers ++ fetchers
|
||||
Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
|
||||
end
|
||||
|
||||
defp get_cc_ap_ids(ap_id, recipients) do
|
||||
|
|
|
|||
|
|
@ -51,6 +51,20 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|
|||
|
||||
def publish(_), do: {:error, "Not implemented"}
|
||||
|
||||
@spec list() :: {:ok, [String.t()]} | {:error, any()}
|
||||
def list do
|
||||
with %User{following: following} = _user <- get_actor() do
|
||||
list =
|
||||
following
|
||||
|> Enum.map(fn entry -> URI.parse(entry).host end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, list}
|
||||
else
|
||||
error -> format_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_error({:error, error}), do: format_error(error)
|
||||
|
||||
defp format_error(error) do
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
|
|
@ -532,7 +532,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
type: "Accept",
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
local: false,
|
||||
activity_id: id
|
||||
})
|
||||
else
|
||||
_e -> :error
|
||||
|
|
@ -540,7 +541,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,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
|
|
@ -555,7 +556,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
type: "Reject",
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
local: false,
|
||||
activity_id: id
|
||||
}) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
|
@ -640,7 +642,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||
# place.
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
|
||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
object_id = Utils.get_ap_id(object_id)
|
||||
|
|
@ -649,7 +651,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:ok, object} <- get_obj_helper(object_id),
|
||||
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
||||
{:ok, activity} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity} <-
|
||||
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
|
||||
{:ok, activity}
|
||||
else
|
||||
nil ->
|
||||
|
|
@ -1053,7 +1056,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
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})
|
||||
maybe_retire_websub(user.ap_id)
|
||||
|
||||
from(
|
||||
a in Activity,
|
||||
|
|
@ -1095,19 +1097,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
def maybe_retire_websub(ap_id) do
|
||||
# some sanity checks
|
||||
if is_binary(ap_id) && String.length(ap_id) > 8 do
|
||||
q =
|
||||
from(
|
||||
ws in Pleroma.Web.Websub.WebsubClientSubscription,
|
||||
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
|
||||
)
|
||||
|
||||
Repo.delete_all(q)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||
Map.put(data, "url", url["href"])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
:user_delete,
|
||||
:users_create,
|
||||
:user_toggle_activation,
|
||||
:user_activate,
|
||||
:user_deactivate,
|
||||
:tag_users,
|
||||
:untag_users,
|
||||
:right_add,
|
||||
|
|
@ -98,7 +100,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
|
|
@ -106,6 +108,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> json(nickname)
|
||||
end
|
||||
|
||||
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
User.delete(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
conn
|
||||
|> json(nicknames)
|
||||
end
|
||||
|
||||
def user_follow(%{assigns: %{user: admin}} = conn, %{
|
||||
"follower" => follower_nick,
|
||||
"followed" => followed_nick
|
||||
|
|
@ -240,7 +256,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
action: action
|
||||
})
|
||||
|
||||
|
|
@ -249,6 +265,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> render("show.json", %{user: updated_user})
|
||||
end
|
||||
|
||||
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.deactivate(users, false)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "activate"
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("index.json", %{users: Keyword.values(updated_users)})
|
||||
end
|
||||
|
||||
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.deactivate(users, true)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "deactivate"
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("index.json", %{users: Keyword.values(updated_users)})
|
||||
end
|
||||
|
||||
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags) do
|
||||
ModerationLog.insert_log(%{
|
||||
|
|
@ -313,6 +359,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
|> Enum.into(%{}, &{&1, true})
|
||||
end
|
||||
|
||||
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
|
||||
"permission_group" => permission_group,
|
||||
"nicknames" => nicknames
|
||||
})
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
info = Map.put(%{}, "is_" <> permission_group, true)
|
||||
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.update_info(users, &User.Info.admin_api_update(&1, info))
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "grant",
|
||||
actor: admin,
|
||||
subject: users,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_add_multiple(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def right_add(%{assigns: %{user: admin}} = conn, %{
|
||||
"permission_group" => permission_group,
|
||||
"nickname" => nickname
|
||||
|
|
@ -328,7 +399,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
ModerationLog.insert_log(%{
|
||||
action: "grant",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
|
|
@ -349,8 +420,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
})
|
||||
end
|
||||
|
||||
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
||||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||
def right_delete_multiple(
|
||||
%{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
|
||||
%{
|
||||
"permission_group" => permission_group,
|
||||
"nicknames" => nicknames
|
||||
}
|
||||
)
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
with false <- Enum.member?(nicknames, admin_nickname) do
|
||||
info = Map.put(%{}, "is_" <> permission_group, false)
|
||||
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.update_info(users, &User.Info.admin_api_update(&1, info))
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "revoke",
|
||||
actor: admin,
|
||||
subject: users,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
else
|
||||
_ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
|
||||
end
|
||||
end
|
||||
|
||||
def right_delete_multiple(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def right_delete(
|
||||
|
|
@ -371,33 +470,24 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
ModerationLog.insert_log(%{
|
||||
action: "revoke",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_delete(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
||||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||
end
|
||||
|
||||
def set_activation_status(%{assigns: %{user: admin}} = conn, %{
|
||||
"nickname" => nickname,
|
||||
"status" => status
|
||||
}) do
|
||||
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
|
||||
%User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, _} <- User.deactivate(user, !status) do
|
||||
action = if(user.info.deactivated, do: "activate", else: "deactivate")
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
action: action
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
def relay_list(conn, _params) do
|
||||
with {:ok, list} <- Relay.list() do
|
||||
json(conn, %{relays: list})
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(500)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
|
|||
}
|
||||
end
|
||||
|
||||
def render("index.json", %{users: users}) do
|
||||
%{
|
||||
users: render_many(users, AccountView, "show.json", as: :user)
|
||||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user}) do
|
||||
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||
display_name = HTML.strip_tags(user.name || user.nickname)
|
||||
|
|
|
|||
|
|
@ -10,19 +10,11 @@ defmodule Pleroma.Web.Federator do
|
|||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.Websub
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
alias Pleroma.Workers.ReceiverWorker
|
||||
alias Pleroma.Workers.SubscriberWorker
|
||||
|
||||
require Logger
|
||||
|
||||
def init do
|
||||
# To do: consider removing this call in favor of scheduled execution (`quantum`-based)
|
||||
refresh_subscriptions(schedule_in: 60)
|
||||
end
|
||||
|
||||
@doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)"
|
||||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
|
||||
def allowed_incoming_reply_depth?(depth) do
|
||||
|
|
@ -37,10 +29,6 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
# Client API
|
||||
|
||||
def incoming_doc(doc) do
|
||||
ReceiverWorker.enqueue("incoming_doc", %{"body" => doc})
|
||||
end
|
||||
|
||||
def incoming_ap_doc(params) do
|
||||
ReceiverWorker.enqueue("incoming_ap_doc", %{"params" => params})
|
||||
end
|
||||
|
|
@ -53,18 +41,6 @@ defmodule Pleroma.Web.Federator do
|
|||
PublisherWorker.enqueue("publish", %{"activity_id" => activity.id})
|
||||
end
|
||||
|
||||
def verify_websub(websub) do
|
||||
SubscriberWorker.enqueue("verify_websub", %{"websub_id" => websub.id})
|
||||
end
|
||||
|
||||
def request_subscription(websub) do
|
||||
SubscriberWorker.enqueue("request_subscription", %{"websub_id" => websub.id})
|
||||
end
|
||||
|
||||
def refresh_subscriptions(worker_args \\ []) do
|
||||
SubscriberWorker.enqueue("refresh_subscriptions", %{}, worker_args ++ [max_attempts: 1])
|
||||
end
|
||||
|
||||
# Job Worker Callbacks
|
||||
|
||||
@spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
|
||||
|
|
@ -81,11 +57,6 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def perform(:incoming_doc, doc) do
|
||||
Logger.info("Got document, trying to parse")
|
||||
OStatus.handle_incoming(doc)
|
||||
end
|
||||
|
||||
def perform(:incoming_ap_doc, params) do
|
||||
Logger.info("Handling incoming AP activity")
|
||||
|
||||
|
|
@ -111,29 +82,6 @@ defmodule Pleroma.Web.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
def perform(:request_subscription, websub) do
|
||||
Logger.debug("Refreshing #{websub.topic}")
|
||||
|
||||
with {:ok, websub} <- Websub.request_subscription(websub) do
|
||||
Logger.debug("Successfully refreshed #{websub.topic}")
|
||||
else
|
||||
_e -> Logger.debug("Couldn't refresh #{websub.topic}")
|
||||
end
|
||||
end
|
||||
|
||||
def perform(:verify_websub, websub) do
|
||||
Logger.debug(fn ->
|
||||
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
|
||||
end)
|
||||
|
||||
Websub.verify(websub)
|
||||
end
|
||||
|
||||
def perform(:refresh_subscriptions) do
|
||||
Logger.debug("Federator running refresh subscriptions")
|
||||
Websub.refresh_subscriptions()
|
||||
end
|
||||
|
||||
def ap_enabled_actor(id) do
|
||||
user = User.get_cached_by_ap_id(id)
|
||||
|
||||
|
|
|
|||
|
|
@ -80,4 +80,30 @@ defmodule Pleroma.Web.Federator.Publisher do
|
|||
links ++ module.gather_nodeinfo_protocol_names()
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gathers a set of remote users given an IR envelope.
|
||||
"""
|
||||
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
||||
bcc =
|
||||
data
|
||||
|> Map.get("bcc", [])
|
||||
|> Enum.reduce([], fn ap_id, bcc ->
|
||||
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||
%Pleroma.List{user_id: ^user_id} = list ->
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
bcc ++ Enum.map(following, & &1.ap_id)
|
||||
|
||||
_ ->
|
||||
bcc
|
||||
end
|
||||
end)
|
||||
|
||||
[to, cc, bcc]
|
||||
|> Enum.concat()
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ defmodule Pleroma.Web.MastoFEController do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "GET /web/manifest.json"
|
||||
def manifest(conn, _params) do
|
||||
conn
|
||||
|> render("manifest.json")
|
||||
end
|
||||
|
||||
@doc "PUT /api/web/settings"
|
||||
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
|
||||
with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
|
||||
|
|
|
|||
|
|
@ -167,7 +167,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|
|||
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
|
||||
true <- Visibility.visible_for_user?(activity, user) do
|
||||
try_render(conn, "show.json", activity: activity, for: user)
|
||||
try_render(conn, "show.json",
|
||||
activity: activity,
|
||||
for: user,
|
||||
with_direct_conversation_id: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
defp cast_params(params) do
|
||||
param_types = %{
|
||||
exclude_types: {:array, :string},
|
||||
exclude_visibilities: {:array, :string},
|
||||
reblogs: :boolean,
|
||||
with_muted: :boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
{_, stream} <- List.keyfind(params, "stream", 0),
|
||||
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
|
||||
topic when is_binary(topic) <- expand_topic(stream, params) do
|
||||
req =
|
||||
if sec_websocket do
|
||||
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
|
||||
else
|
||||
req
|
||||
end
|
||||
|
||||
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
|
||||
else
|
||||
{:error, code} ->
|
||||
|
|
|
|||
|
|
@ -1,313 +0,0 @@
|
|||
# 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
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OStatus.UserRepresenter
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
defp get_href(id) do
|
||||
with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do
|
||||
external_url
|
||||
else
|
||||
_e -> id
|
||||
end
|
||||
end
|
||||
|
||||
defp get_in_reply_to(activity) do
|
||||
with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do
|
||||
[
|
||||
{:"thr:in-reply-to",
|
||||
[ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
|
||||
]
|
||||
else
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp get_mentions(to) do
|
||||
Enum.map(to, fn id ->
|
||||
cond do
|
||||
# Special handling for the AP/Ostatus public collections
|
||||
Pleroma.Constants.as_public() == id ->
|
||||
{:link,
|
||||
[
|
||||
rel: "mentioned",
|
||||
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection",
|
||||
href: "http://activityschema.org/collection/public"
|
||||
], []}
|
||||
|
||||
# Ostatus doesn't handle follower collections, ignore these.
|
||||
Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) ->
|
||||
[]
|
||||
|
||||
true ->
|
||||
{:link,
|
||||
[
|
||||
rel: "mentioned",
|
||||
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/person",
|
||||
href: id
|
||||
], []}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_links(%{local: true}, %{"id" => object_id}) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
[
|
||||
{:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
|
||||
{:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
|
||||
]
|
||||
end
|
||||
|
||||
defp get_links(%{local: false}, %{"external_url" => external_url}) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
[
|
||||
{:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []}
|
||||
]
|
||||
end
|
||||
|
||||
defp get_links(_activity, _object_data), do: []
|
||||
|
||||
defp get_emoji_links(emojis) do
|
||||
Enum.map(emojis, fn {emoji, file} ->
|
||||
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
|
||||
end)
|
||||
end
|
||||
|
||||
def to_simple_form(activity, user, with_author \\ false)
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
object = Object.normalize(activity)
|
||||
|
||||
updated_at = object.data["published"]
|
||||
inserted_at = object.data["published"]
|
||||
|
||||
attachments =
|
||||
Enum.map(object.data["attachment"] || [], fn attachment ->
|
||||
url = hd(attachment["url"])
|
||||
|
||||
{:link,
|
||||
[rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])],
|
||||
[]}
|
||||
end)
|
||||
|
||||
in_reply_to = get_in_reply_to(activity)
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
mentions = activity.recipients |> get_mentions
|
||||
|
||||
categories =
|
||||
(object.data["tag"] || [])
|
||||
|> Enum.map(fn tag ->
|
||||
if is_binary(tag) do
|
||||
{:category, [term: to_charlist(tag)], []}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
emoji_links = get_emoji_links(object.data["emoji"] || %{})
|
||||
|
||||
summary =
|
||||
if object.data["summary"] do
|
||||
[{:summary, [], h.(object.data["summary"])}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
|
||||
# For notes, federate the object id.
|
||||
{:id, h.(object.data["id"])},
|
||||
{:title, ['New note by #{user.nickname}']},
|
||||
{:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
|
||||
] ++
|
||||
summary ++
|
||||
get_links(activity, object.data) ++
|
||||
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
mentions = activity.recipients |> get_mentions
|
||||
|
||||
[
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['New favorite by #{user.nickname}']},
|
||||
{:content, [type: 'html'], ['#{user.nickname} favorited something']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
|
||||
# For notes, federate the object id.
|
||||
{:id, h.(activity.data["object"])}
|
||||
]},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
|
||||
{:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []}
|
||||
] ++ author ++ mentions
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
|
||||
retweeted_object = Object.normalize(retweeted_activity)
|
||||
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
|
||||
|
||||
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
|
||||
|
||||
mentions =
|
||||
([retweeted_user.ap_id] ++ activity.recipients)
|
||||
|> Enum.uniq()
|
||||
|> get_mentions()
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} repeated a notice']},
|
||||
{:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
|
||||
{:"activity:object", retweeted_xml}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
mentions = (activity.recipients || []) |> get_mentions
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} started following #{activity.data["object"]}']},
|
||||
{:content, [type: 'html'],
|
||||
['#{user.nickname} started following #{activity.data["object"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:id, h.(activity.data["object"])},
|
||||
{:uri, h.(activity.data["object"])}
|
||||
]},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
# Only undos of follow for now. Will need to get redone once there are more
|
||||
def to_simple_form(
|
||||
%{data: %{"type" => "Undo", "object" => %{"type" => "Follow"} = follow_activity}} =
|
||||
activity,
|
||||
user,
|
||||
with_author
|
||||
) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
mentions = (activity.recipients || []) |> get_mentions
|
||||
follow_activity = Activity.normalize(follow_activity)
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
|
||||
{:content, [type: 'html'],
|
||||
['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:id, h.(follow_activity.data["object"])},
|
||||
{:uri, h.(follow_activity.data["object"])}
|
||||
]},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/delete']},
|
||||
{:id, h.(activity.data["object"])},
|
||||
{:title, ['An object was deleted']},
|
||||
{:content, [type: 'html'], ['An object was deleted']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)}
|
||||
] ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(_, _, _), do: nil
|
||||
|
||||
def wrap_with_entry(simple_form) do
|
||||
[
|
||||
{
|
||||
:entry,
|
||||
[
|
||||
xmlns: 'http://www.w3.org/2005/Atom',
|
||||
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
|
||||
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
|
||||
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
|
||||
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
|
||||
],
|
||||
simple_form
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# 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.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 =
|
||||
(List.first(activities) || user).updated_at
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
last_activity = List.last(activities)
|
||||
|
||||
entries =
|
||||
activities
|
||||
|> Enum.map(fn activity ->
|
||||
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
|
||||
end)
|
||||
|> Enum.filter(fn {_, form} -> form end)
|
||||
|
||||
[
|
||||
{
|
||||
:feed,
|
||||
[
|
||||
xmlns: 'http://www.w3.org/2005/Atom',
|
||||
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
|
||||
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
|
||||
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
|
||||
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
|
||||
],
|
||||
[
|
||||
{:id, h.(OStatus.feed_path(user))},
|
||||
{:title, ['#{user.nickname}\'s timeline']},
|
||||
{:updated, h.(most_recent_update)},
|
||||
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
|
||||
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
|
||||
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
|
||||
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'],
|
||||
[]},
|
||||
{:author, UserRepresenter.to_simple_form(user)}
|
||||
] ++
|
||||
if last_activity do
|
||||
[
|
||||
{:link,
|
||||
[
|
||||
rel: 'next',
|
||||
href:
|
||||
to_charlist(OStatus.feed_path(user)) ++
|
||||
'?max_id=' ++ to_charlist(last_activity.id),
|
||||
type: 'application/atom+xml'
|
||||
], []}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end ++ entries
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 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.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),
|
||||
%Object{} = object <- Object.normalize(id),
|
||||
{:ok, delete} <- ActivityPub.delete(object, false) do
|
||||
delete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# 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.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_actor(doc),
|
||||
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
|
||||
followed_uri when not is_nil(followed_uri) <-
|
||||
XML.string_from_xpath("/entry/activity:object/id", entry),
|
||||
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
|
||||
{:locked, false} <- {:locked, followed.info.locked},
|
||||
{:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do
|
||||
User.follow(actor, followed)
|
||||
{:ok, activity}
|
||||
else
|
||||
{:locked, true} ->
|
||||
{:error, "It's not possible to follow locked accounts over OStatus"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
# 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
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
@doc """
|
||||
Get the context for this note. Uses this:
|
||||
1. The context of the parent activity
|
||||
2. The conversation reference in the ostatus xml
|
||||
3. A newly generated context id.
|
||||
"""
|
||||
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(in_reply_to) do
|
||||
context
|
||||
else
|
||||
_e ->
|
||||
if String.length(context) > 0 do
|
||||
context
|
||||
else
|
||||
Utils.generate_context_id()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_people_mentions(entry) do
|
||||
:xmerl_xpath.string(
|
||||
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]',
|
||||
entry
|
||||
)
|
||||
|> Enum.map(fn person -> XML.string_from_xpath("@href", person) end)
|
||||
end
|
||||
|
||||
def get_collection_mentions(entry) do
|
||||
transmogrify = fn
|
||||
"http://activityschema.org/collection/public" ->
|
||||
Pleroma.Constants.as_public()
|
||||
|
||||
group ->
|
||||
group
|
||||
end
|
||||
|
||||
:xmerl_xpath.string(
|
||||
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]',
|
||||
entry
|
||||
)
|
||||
|> Enum.map(fn collection -> XML.string_from_xpath("@href", collection) |> transmogrify.() end)
|
||||
end
|
||||
|
||||
def get_mentions(entry) do
|
||||
(get_people_mentions(entry) ++ get_collection_mentions(entry))
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
def get_emoji(entry) do
|
||||
try do
|
||||
:xmerl_xpath.string('//link[@rel="emoji"]', entry)
|
||||
|> Enum.reduce(%{}, fn emoji, acc ->
|
||||
Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji))
|
||||
end)
|
||||
rescue
|
||||
_e -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def make_to_list(actor, mentions) do
|
||||
[
|
||||
actor.follower_address
|
||||
] ++ mentions
|
||||
end
|
||||
|
||||
def add_external_url(note, entry) do
|
||||
url = XML.string_from_xpath("//link[@rel='alternate' and @type='text/html']/@href", entry)
|
||||
Map.put(note, "external_url", url)
|
||||
end
|
||||
|
||||
def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do
|
||||
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
|
||||
activity
|
||||
else
|
||||
_e ->
|
||||
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
|
||||
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(in_reply_to_href, options) do
|
||||
activity
|
||||
else
|
||||
_e -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Clean this up a bit.
|
||||
def handle_note(entry, doc \\ nil, options \\ []) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
|
||||
[author] <- :xmerl_xpath.string('//author[1]', doc),
|
||||
{:ok, actor} <- OStatus.find_make_or_update_actor(author),
|
||||
content_html <- OStatus.get_content(entry),
|
||||
cw <- OStatus.get_cw(entry),
|
||||
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
|
||||
options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1),
|
||||
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options),
|
||||
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, in_reply_to),
|
||||
tags <- OStatus.get_tags(entry),
|
||||
mentions <- get_mentions(entry),
|
||||
to <- make_to_list(actor, mentions),
|
||||
date <- XML.string_from_xpath("//published", entry),
|
||||
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
|
||||
cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []),
|
||||
note <-
|
||||
CommonAPI.Utils.make_note_data(
|
||||
actor.ap_id,
|
||||
to,
|
||||
context,
|
||||
content_html,
|
||||
attachments,
|
||||
in_reply_to_activity,
|
||||
[],
|
||||
cw
|
||||
),
|
||||
note <- note |> Map.put("id", id) |> Map.put("tag", tags),
|
||||
note <- note |> Map.put("published", date),
|
||||
note <- note |> Map.put("emoji", get_emoji(entry)),
|
||||
note <- add_external_url(note, entry),
|
||||
note <- note |> Map.put("cc", cc),
|
||||
# TODO: Handle this case in make_note_data
|
||||
note <-
|
||||
if(
|
||||
in_reply_to && !in_reply_to_activity,
|
||||
do: note |> Map.put("inReplyTo", in_reply_to),
|
||||
else: note
|
||||
) do
|
||||
ActivityPub.create(%{
|
||||
to: to,
|
||||
actor: actor,
|
||||
context: context,
|
||||
object: note,
|
||||
published: date,
|
||||
local: false,
|
||||
additional: %{"cc" => cc}
|
||||
})
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# 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.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_actor(doc),
|
||||
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
|
||||
followed_uri when not is_nil(followed_uri) <-
|
||||
XML.string_from_xpath("/entry/activity:object/id", entry),
|
||||
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
|
||||
{:ok, activity} <- ActivityPub.unfollow(actor, followed, id, false) do
|
||||
User.unfollow(actor, followed)
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
# 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
|
||||
import Pleroma.Web.XML
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
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{} = activity) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cond do
|
||||
is_nil(object) ->
|
||||
false
|
||||
|
||||
Visibility.is_public?(activity) && object.data["type"] == "Note" ->
|
||||
true
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def feed_path(user), do: "#{user.ap_id}/feed.atom"
|
||||
|
||||
def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
|
||||
|
||||
def salmon_path(user), do: "#{user.ap_id}/salmon"
|
||||
|
||||
def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
|
||||
|
||||
def handle_incoming(xml_string, options \\ []) do
|
||||
with doc when doc != :error <- parse_document(xml_string) do
|
||||
with {:ok, actor_user} <- find_make_or_update_actor(doc),
|
||||
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
|
||||
|
||||
entries = :xmerl_xpath.string('//entry', doc)
|
||||
|
||||
activities =
|
||||
Enum.map(entries, fn entry ->
|
||||
{:xmlObj, :string, object_type} =
|
||||
:xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
|
||||
|
||||
{:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
|
||||
Logger.debug("Handling #{verb}")
|
||||
|
||||
try do
|
||||
case verb do
|
||||
'http://activitystrea.ms/schema/1.0/delete' ->
|
||||
with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/follow' ->
|
||||
with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/unfollow' ->
|
||||
with {:ok, activity} <- UnfollowHandler.handle(entry, doc), do: activity
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/share' ->
|
||||
with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
|
||||
do: [activity, retweeted_activity]
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/favorite' ->
|
||||
with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
|
||||
do: [activity, favorited_activity]
|
||||
|
||||
_ ->
|
||||
case object_type do
|
||||
'http://activitystrea.ms/schema/1.0/note' ->
|
||||
with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
|
||||
do: activity
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/comment' ->
|
||||
with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
|
||||
do: activity
|
||||
|
||||
_ ->
|
||||
Logger.error("Couldn't parse incoming document")
|
||||
nil
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Error occured while handling activity")
|
||||
Logger.error(xml_string)
|
||||
Logger.error(inspect(e))
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
{:ok, activities}
|
||||
else
|
||||
_e -> {:error, []}
|
||||
end
|
||||
end
|
||||
|
||||
def make_share(entry, doc, retweeted_activity) do
|
||||
with {:ok, actor} <- find_make_or_update_actor(doc),
|
||||
%Object{} = object <- Object.normalize(retweeted_activity),
|
||||
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
|
||||
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_share(entry, doc) do
|
||||
with {:ok, retweeted_activity} <- get_or_build_object(entry),
|
||||
{:ok, activity} <- make_share(entry, doc, retweeted_activity) do
|
||||
{:ok, activity, retweeted_activity}
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def make_favorite(entry, doc, favorited_activity) do
|
||||
with {:ok, actor} <- find_make_or_update_actor(doc),
|
||||
%Object{} = object <- Object.normalize(favorited_activity),
|
||||
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
|
||||
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def get_or_build_object(entry) do
|
||||
with {:ok, activity} <- get_or_try_fetching(entry) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
|
||||
NoteHandler.handle_note(object, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_or_try_fetching(entry) 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_by_object_ap_id_with_object(id) do
|
||||
{:ok, activity}
|
||||
else
|
||||
_ ->
|
||||
Logger.debug("Couldn't get, will try to fetch")
|
||||
|
||||
with href when not is_nil(href) <-
|
||||
string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
|
||||
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
|
||||
{:ok, favorited_activity}
|
||||
else
|
||||
e -> Logger.debug("Couldn't find href: #{inspect(e)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_favorite(entry, doc) do
|
||||
with {:ok, favorited_activity} <- get_or_try_fetching(entry),
|
||||
{:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
|
||||
{:ok, activity, favorited_activity}
|
||||
else
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def get_attachments(entry) do
|
||||
:xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
|
||||
|> Enum.map(fn enclosure ->
|
||||
with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
|
||||
type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
|
||||
%{
|
||||
"type" => "Attachment",
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => type,
|
||||
"href" => href
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the content from a an entry.
|
||||
"""
|
||||
def get_content(entry) do
|
||||
string_from_xpath("//content", entry)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the cw that mastodon uses.
|
||||
"""
|
||||
def get_cw(entry) do
|
||||
case string_from_xpath("/*/summary", entry) do
|
||||
cw when not is_nil(cw) -> cw
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_tags(entry) do
|
||||
:xmerl_xpath.string('//category', entry)
|
||||
|> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.map(&String.downcase/1)
|
||||
end
|
||||
|
||||
def maybe_update(doc, user) do
|
||||
case string_from_xpath("//author[1]/ap_enabled", doc) do
|
||||
"true" ->
|
||||
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
|
||||
|
||||
_ ->
|
||||
maybe_update_ostatus(doc, user)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_update_ostatus(doc, user) do
|
||||
old_data = Map.take(user, [:bio, :avatar, :name])
|
||||
|
||||
with false <- user.local,
|
||||
avatar <- make_avatar_object(doc),
|
||||
bio <- string_from_xpath("//author[1]/summary", doc),
|
||||
name <- string_from_xpath("//author[1]/poco:displayName", doc),
|
||||
new_data <- %{
|
||||
avatar: avatar || old_data.avatar,
|
||||
name: name || old_data.name,
|
||||
bio: bio || old_data.bio
|
||||
},
|
||||
false <- new_data == old_data do
|
||||
change = Ecto.Changeset.change(user, new_data)
|
||||
User.update_and_set_cache(change)
|
||||
else
|
||||
_ ->
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
def find_make_or_update_actor(doc) do
|
||||
uri = string_from_xpath("//author/uri[1]", doc)
|
||||
|
||||
with {:ok, %User{} = user} <- find_or_make_user(uri),
|
||||
{:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do
|
||||
maybe_update(doc, user)
|
||||
else
|
||||
{:ap_enabled, true} ->
|
||||
{:error, :invalid_protocol}
|
||||
|
||||
_ ->
|
||||
{:error, :unknown_user}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_or_make_user(String.t()) :: {:ok, User.t()}
|
||||
def find_or_make_user(uri) do
|
||||
case User.get_by_ap_id(uri) do
|
||||
%User{} = user -> {:ok, user}
|
||||
_ -> make_user(uri)
|
||||
end
|
||||
end
|
||||
|
||||
@spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()}
|
||||
def make_user(uri, update \\ false) do
|
||||
with {:ok, info} <- gather_user_info(uri) do
|
||||
with false <- update,
|
||||
%User{} = user <- User.get_cached_by_ap_id(info["uri"]) do
|
||||
{:ok, user}
|
||||
else
|
||||
_e -> User.insert_or_update_user(build_user_data(info))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp build_user_data(info) do
|
||||
%{
|
||||
name: info["name"],
|
||||
nickname: info["nickname"] <> "@" <> info["host"],
|
||||
ap_id: info["uri"],
|
||||
info: info,
|
||||
avatar: info["avatar"],
|
||||
bio: info["bio"]
|
||||
}
|
||||
end
|
||||
|
||||
# TODO: Just takes the first one for now.
|
||||
def make_avatar_object(author_doc, rel \\ "avatar") do
|
||||
href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
|
||||
type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
|
||||
|
||||
if href do
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => [%{"type" => "Link", "mediaType" => type, "href" => href}]
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()}
|
||||
def gather_user_info(username) do
|
||||
with {:ok, webfinger_data} <- WebFinger.finger(username),
|
||||
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
|
||||
data =
|
||||
webfinger_data
|
||||
|> Map.merge(feed_data)
|
||||
|> Map.put("fqn", username)
|
||||
|
||||
{:ok, data}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(fn -> "Couldn't gather info for #{username}" end)
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
# Regex-based 'parsing' so we don't have to pull in a full html parser
|
||||
# It's a hack anyway. Maybe revisit this in the future
|
||||
@mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
|
||||
@gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
|
||||
@gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
|
||||
def get_atom_url(body) do
|
||||
cond do
|
||||
Regex.match?(@mastodon_regex, body) ->
|
||||
[[_, match]] = Regex.scan(@mastodon_regex, body)
|
||||
{:ok, match}
|
||||
|
||||
Regex.match?(@gs_regex, body) ->
|
||||
[[_, match]] = Regex.scan(@gs_regex, body)
|
||||
{:ok, match}
|
||||
|
||||
Regex.match?(@gs_classic_regex, body) ->
|
||||
[[_, match]] = Regex.scan(@gs_classic_regex, body)
|
||||
{:ok, match}
|
||||
|
||||
true ->
|
||||
Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
|
||||
{:error, "Couldn't find the Atom link"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activity_from_atom_url(url, options \\ []) do
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
HTTP.get(url, [{:Accept, "application/atom+xml"}]) do
|
||||
Logger.debug("Got document from #{url}, handling...")
|
||||
handle_incoming(body, options)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activity_from_html_url(url, options \\ []) do
|
||||
Logger.debug("Trying to fetch #{url}")
|
||||
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body}} <- HTTP.get(url, []),
|
||||
{:ok, atom_url} <- get_atom_url(body) do
|
||||
fetch_activity_from_atom_url(atom_url, options)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activity_from_url(url, options \\ []) do
|
||||
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e -> fetch_activity_from_html_url(url, options)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
end
|
||||
end
|
||||
|
|
@ -13,19 +13,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
alias Pleroma.Web.ActivityPub.ObjectView
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.Metadata.PlayerView
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.Router
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.RateLimiter,
|
||||
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.SetFormatPlug
|
||||
when action in [:object, :activity, :notice]
|
||||
|
|
@ -33,32 +28,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
|
||||
action_fallback(:errors)
|
||||
|
||||
defp decode_or_retry(body) do
|
||||
with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
|
||||
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
|
||||
{:ok, doc}
|
||||
else
|
||||
_e ->
|
||||
with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
|
||||
doc <- XML.parse_document(decoded),
|
||||
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
|
||||
{:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
|
||||
{:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
|
||||
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
|
||||
{:ok, doc}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def salmon_incoming(conn, _) do
|
||||
{:ok, body, _conn} = read_body(conn)
|
||||
{:ok, doc} = decode_or_retry(body)
|
||||
|
||||
Federator.incoming_doc(doc)
|
||||
|
||||
send_resp(conn, 200, "")
|
||||
end
|
||||
|
||||
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
|
||||
when format in ["json", "activity+json"] do
|
||||
ActivityPubController.call(conn, :object)
|
||||
|
|
@ -179,23 +148,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
|> render("object.json", %{object: object})
|
||||
end
|
||||
|
||||
defp represent_activity(_conn, "activity+json", _, _) do
|
||||
defp represent_activity(_conn, _, _, _) do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
defp represent_activity(conn, _, activity, user) do
|
||||
response =
|
||||
activity
|
||||
|> ActivityRepresenter.to_simple_form(user, true)
|
||||
|> ActivityRepresenter.wrap_with_entry()
|
||||
|> :xmerl.export_simple(:xmerl_xml)
|
||||
|> to_string
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> send_resp(200, response)
|
||||
end
|
||||
|
||||
def errors(conn, {:error, :not_found}) do
|
||||
render_error(conn, :not_found, "Not found")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
# 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
|
||||
|
||||
def to_simple_form(user) do
|
||||
ap_id = to_charlist(user.ap_id)
|
||||
nickname = to_charlist(user.nickname)
|
||||
name = to_charlist(user.name)
|
||||
bio = to_charlist(user.bio)
|
||||
avatar_url = to_charlist(User.avatar_url(user))
|
||||
|
||||
banner =
|
||||
if banner_url = User.banner_url(user) do
|
||||
[{:link, [rel: 'header', href: banner_url], []}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
ap_enabled =
|
||||
if user.local do
|
||||
[{:ap_enabled, ['true']}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
[
|
||||
{:id, [ap_id]},
|
||||
{:"activity:object", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:uri, [ap_id]},
|
||||
{:"poco:preferredUsername", [nickname]},
|
||||
{:"poco:displayName", [name]},
|
||||
{:"poco:note", [bio]},
|
||||
{:summary, [bio]},
|
||||
{:name, [nickname]},
|
||||
{:link, [rel: 'avatar', href: avatar_url], []}
|
||||
] ++ banner ++ ap_enabled
|
||||
end
|
||||
end
|
||||
|
|
@ -79,6 +79,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
|
|||
end
|
||||
end
|
||||
|
||||
def read_conversations(%{assigns: %{user: user}} = conn, _params) do
|
||||
with {:ok, participations} <- Participation.mark_all_as_read(user) do
|
||||
conn
|
||||
|> add_link_headers(participations)
|
||||
|> put_view(ConversationView)
|
||||
|> render("participations.json", participations: participations, for: user)
|
||||
end
|
||||
end
|
||||
|
||||
def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
|
||||
with {:ok, notification} <- Notification.read_one(user, notification_id) do
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do
|
|||
delete("/users", AdminAPIController, :user_delete)
|
||||
post("/users", AdminAPIController, :users_create)
|
||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||
patch("/users/activate", AdminAPIController, :user_activate)
|
||||
patch("/users/deactivate", AdminAPIController, :user_deactivate)
|
||||
put("/users/tag", AdminAPIController, :tag_users)
|
||||
delete("/users/tag", AdminAPIController, :untag_users)
|
||||
|
||||
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
|
||||
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
|
||||
|
||||
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
|
||||
|
||||
delete(
|
||||
|
|
@ -150,8 +153,15 @@ defmodule Pleroma.Web.Router do
|
|||
:right_delete
|
||||
)
|
||||
|
||||
put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
|
||||
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
|
||||
|
||||
delete(
|
||||
"/users/permission_group/:permission_group",
|
||||
AdminAPIController,
|
||||
:right_delete_multiple
|
||||
)
|
||||
|
||||
get("/relay", AdminAPIController, :relay_list)
|
||||
post("/relay", AdminAPIController, :relay_follow)
|
||||
delete("/relay", AdminAPIController, :relay_unfollow)
|
||||
|
||||
|
|
@ -256,6 +266,7 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
|
||||
get("/conversations/:id", PleromaAPIController, :conversation)
|
||||
post("/conversations/read", PleromaAPIController, :read_conversations)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
|
|
@ -499,11 +510,6 @@ defmodule Pleroma.Web.Router do
|
|||
get("/users/:nickname/feed", Feed.FeedController, :feed)
|
||||
get("/users/:nickname", Feed.FeedController, :feed_redirect)
|
||||
|
||||
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
|
||||
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
|
||||
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
|
||||
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
|
||||
|
||||
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
|
||||
end
|
||||
|
||||
|
|
@ -586,6 +592,12 @@ defmodule Pleroma.Web.Router do
|
|||
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
pipe_through(:api)
|
||||
|
||||
get("/web/manifest.json", MastoFEController, :manifest)
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web do
|
||||
pipe_through(:mastodon_html)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,254 +0,0 @@
|
|||
# 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
|
||||
@behaviour Pleroma.Web.Federator.Publisher
|
||||
|
||||
use Bitwise
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
require Logger
|
||||
|
||||
def decode(salmon) do
|
||||
doc = XML.parse_document(salmon)
|
||||
|
||||
{:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
|
||||
{:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
|
||||
{:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
|
||||
{:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
|
||||
{:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
|
||||
|
||||
{:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
|
||||
{:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
|
||||
alg = to_string(alg)
|
||||
encoding = to_string(encoding)
|
||||
type = to_string(type)
|
||||
|
||||
[data, type, encoding, alg, sig]
|
||||
end
|
||||
|
||||
def fetch_magic_key(salmon) do
|
||||
with [data, _, _, _, _] <- decode(salmon),
|
||||
doc <- XML.parse_document(data),
|
||||
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
|
||||
{:ok, public_key} <- User.get_public_key_for_ap_id(uri),
|
||||
magic_key <- encode_key(public_key) do
|
||||
{:ok, magic_key}
|
||||
end
|
||||
end
|
||||
|
||||
def decode_and_validate(magickey, salmon) do
|
||||
[data, type, encoding, alg, sig] = decode(salmon)
|
||||
|
||||
signed_text =
|
||||
[data, type, encoding, alg]
|
||||
|> Enum.map(&Base.url_encode64/1)
|
||||
|> Enum.join(".")
|
||||
|
||||
key = decode_key(magickey)
|
||||
|
||||
verify = :public_key.verify(signed_text, :sha256, sig, key)
|
||||
|
||||
if verify do
|
||||
{:ok, data}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def decode_key("RSA." <> magickey) do
|
||||
make_integer = fn bin ->
|
||||
list = :erlang.binary_to_list(bin)
|
||||
Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
|
||||
end
|
||||
|
||||
[modulus, exponent] =
|
||||
magickey
|
||||
|> String.split(".")
|
||||
|> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
|
||||
|> Enum.map(make_integer)
|
||||
|
||||
{:RSAPublicKey, modulus, exponent}
|
||||
end
|
||||
|
||||
def encode_key({:RSAPublicKey, modulus, exponent}) do
|
||||
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
|
||||
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
|
||||
|
||||
"RSA.#{modulus_enc}.#{exponent_enc}"
|
||||
end
|
||||
|
||||
def encode(private_key, doc) do
|
||||
type = "application/atom+xml"
|
||||
encoding = "base64url"
|
||||
alg = "RSA-SHA256"
|
||||
|
||||
signed_text =
|
||||
[doc, type, encoding, alg]
|
||||
|> Enum.map(&Base.url_encode64/1)
|
||||
|> Enum.join(".")
|
||||
|
||||
signature =
|
||||
signed_text
|
||||
|> :public_key.sign(:sha256, private_key)
|
||||
|> to_string
|
||||
|> Base.url_encode64()
|
||||
|
||||
doc_base64 =
|
||||
doc
|
||||
|> Base.url_encode64()
|
||||
|
||||
# Don't need proper xml building, these strings are safe to leave unescaped
|
||||
salmon = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
|
||||
<me:data type="application/atom+xml">#{doc_base64}</me:data>
|
||||
<me:encoding>#{encoding}</me:encoding>
|
||||
<me:alg>#{alg}</me:alg>
|
||||
<me:sig>#{signature}</me:sig>
|
||||
</me:env>
|
||||
"""
|
||||
|
||||
{:ok, salmon}
|
||||
end
|
||||
|
||||
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
||||
bcc =
|
||||
data
|
||||
|> Map.get("bcc", [])
|
||||
|> Enum.reduce([], fn ap_id, bcc ->
|
||||
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||
%Pleroma.List{user_id: ^user_id} = list ->
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
bcc ++ Enum.map(following, & &1.ap_id)
|
||||
|
||||
_ ->
|
||||
bcc
|
||||
end
|
||||
end)
|
||||
|
||||
[to, cc, bcc]
|
||||
|> Enum.concat()
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
|
||||
@doc "Pushes an activity to remote account."
|
||||
def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
|
||||
do: publish_one(Map.put(params, :recipient, salmon))
|
||||
|
||||
def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
|
||||
with {:ok, %{status: code}} when code in 200..299 <-
|
||||
HTTP.post(
|
||||
url,
|
||||
feed,
|
||||
[{"Content-Type", "application/magic-envelope+xml"}]
|
||||
) do
|
||||
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, code}
|
||||
else
|
||||
e ->
|
||||
unless params[:unreachable_since], do: Instances.set_reachable(url)
|
||||
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
|
||||
{:error, "Unreachable instance"}
|
||||
end
|
||||
end
|
||||
|
||||
def publish_one(%{recipient_id: recipient_id} = params) do
|
||||
recipient = User.get_cached_by_id(recipient_id)
|
||||
|
||||
params
|
||||
|> Map.delete(:recipient_id)
|
||||
|> Map.put(:recipient, recipient)
|
||||
|> publish_one()
|
||||
end
|
||||
|
||||
def publish_one(_), do: :noop
|
||||
|
||||
@supported_activities [
|
||||
"Create",
|
||||
"Follow",
|
||||
"Like",
|
||||
"Announce",
|
||||
"Undo",
|
||||
"Delete"
|
||||
]
|
||||
|
||||
def is_representable?(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in @supported_activities,
|
||||
do: Visibility.is_public?(activity)
|
||||
|
||||
def is_representable?(_), do: false
|
||||
|
||||
@doc """
|
||||
Publishes an activity to remote accounts
|
||||
"""
|
||||
@spec publish(User.t(), Pleroma.Activity.t()) :: none
|
||||
def publish(user, activity)
|
||||
|
||||
def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity)
|
||||
when type in @supported_activities do
|
||||
feed = ActivityRepresenter.to_simple_form(activity, user, true)
|
||||
|
||||
if feed do
|
||||
feed =
|
||||
ActivityRepresenter.wrap_with_entry(feed)
|
||||
|> :xmerl.export_simple(:xmerl_xml)
|
||||
|> to_string
|
||||
|
||||
{:ok, private, _} = Keys.keys_from_pem(keys)
|
||||
{:ok, feed} = encode(private, feed)
|
||||
|
||||
remote_users = remote_users(user, 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 ->
|
||||
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
|
||||
|
||||
Publisher.enqueue_one(__MODULE__, %{
|
||||
recipient_id: remote_user.id,
|
||||
feed: feed,
|
||||
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
|
||||
|
||||
def gather_webfinger_links(%User{} = user) do
|
||||
{:ok, _private, public} = Keys.keys_from_pem(user.keys)
|
||||
magic_key = encode_key(public)
|
||||
|
||||
[
|
||||
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
|
||||
%{
|
||||
"rel" => "magic-public-key",
|
||||
"href" => "data:application/magic-public-key,#{magic_key}"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def gather_nodeinfo_protocol_names, do: []
|
||||
end
|
||||
|
|
@ -49,7 +49,7 @@ defmodule Pleroma.Web.Streamer do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_should_send(_) do
|
||||
true
|
||||
end
|
||||
defp handle_should_send(:benchmark), do: false
|
||||
|
||||
defp handle_should_send(_), do: true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
<title><%= @user.nickname <> "'s timeline" %></title>
|
||||
<updated><%= most_recent_update(@activities, @user) %></updated>
|
||||
<logo><%= logo(@user) %></logo>
|
||||
<link rel="hub" href="<%= websub_url(@conn, :websub_subscription_request, @user.nickname) %>"/>
|
||||
<link rel="salmon" href="<%= o_status_url(@conn, :salmon_incoming, @user.nickname) %>"/>
|
||||
<link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
|
||||
|
||||
<%= render @view_module, "_author.xml", assigns %>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
<meta charset='utf-8'>
|
||||
<meta content='width=device-width, initial-scale=1' name='viewport'>
|
||||
<title>
|
||||
<%= Pleroma.Config.get([:instance, :name]) %>
|
||||
<%= Config.get([:instance, :name]) %>
|
||||
</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<link rel="manifest" type="applicaton/manifest+json" href="<%= masto_fe_path(Pleroma.Web.Endpoint, :manifest) %>" />
|
||||
|
||||
<meta name="theme-color" content="<%= Config.get([:manifest, :theme_color]) %>" />
|
||||
|
||||
<script crossorigin='anonymous' src="/packs/locales.js"></script>
|
||||
<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>
|
||||
|
||||
|
|
|
|||
|
|
@ -99,4 +99,23 @@ defmodule Pleroma.Web.MastoFEView do
|
|||
defp present?(nil), do: false
|
||||
defp present?(false), do: false
|
||||
defp present?(_), do: true
|
||||
|
||||
def render("manifest.json", _params) do
|
||||
%{
|
||||
name: Config.get([:instance, :name]),
|
||||
description: Config.get([:instance, :description]),
|
||||
icons: Config.get([:manifest, :icons]),
|
||||
theme_color: Config.get([:manifest, :theme_color]),
|
||||
background_color: Config.get([:manifest, :background_color]),
|
||||
display: "standalone",
|
||||
scope: Pleroma.Web.base_url(),
|
||||
start_url: masto_fe_path(Pleroma.Web.Endpoint, :index, ["getting-started"]),
|
||||
categories: [
|
||||
"social"
|
||||
],
|
||||
serviceworker: %{
|
||||
src: "/sw.js"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ defmodule Pleroma.Web.WebFinger do
|
|||
doc
|
||||
),
|
||||
subject <- XML.string_from_xpath("//Subject", doc),
|
||||
salmon <- XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc),
|
||||
subscribe_address <-
|
||||
XML.string_from_xpath(
|
||||
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
|
||||
|
|
@ -123,7 +122,6 @@ defmodule Pleroma.Web.WebFinger do
|
|||
"magic_key" => magic_key,
|
||||
"topic" => topic,
|
||||
"subject" => subject,
|
||||
"salmon" => salmon,
|
||||
"subscribe_address" => subscribe_address,
|
||||
"ap_id" => ap_id
|
||||
}
|
||||
|
|
@ -148,16 +146,6 @@ defmodule Pleroma.Web.WebFinger do
|
|||
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
|
||||
Map.put(data, "ap_id", link["href"])
|
||||
|
||||
{_, "magic-public-key"} ->
|
||||
"data:application/magic-public-key," <> magic_key = link["href"]
|
||||
Map.put(data, "magic_key", magic_key)
|
||||
|
||||
{"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
|
||||
Map.put(data, "topic", link["href"])
|
||||
|
||||
{_, "salmon"} ->
|
||||
Map.put(data, "salmon", link["href"])
|
||||
|
||||
{_, "http://ostatus.org/schema/1.0/subscribe"} ->
|
||||
Map.put(data, "subscribe_address", link["template"])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,332 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Websub do
|
||||
alias Ecto.Changeset
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.FeedRepresenter
|
||||
alias Pleroma.Web.Router.Helpers
|
||||
alias Pleroma.Web.Websub.WebsubClientSubscription
|
||||
alias Pleroma.Web.Websub.WebsubServerSubscription
|
||||
alias Pleroma.Web.XML
|
||||
require Logger
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
@behaviour Pleroma.Web.Federator.Publisher
|
||||
|
||||
def verify(subscription, getter \\ &HTTP.get/3) do
|
||||
challenge = Base.encode16(:crypto.strong_rand_bytes(8))
|
||||
lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
|
||||
lease_seconds = lease_seconds |> to_string
|
||||
|
||||
params = %{
|
||||
"hub.challenge": challenge,
|
||||
"hub.lease_seconds": lease_seconds,
|
||||
"hub.topic": subscription.topic,
|
||||
"hub.mode": "subscribe"
|
||||
}
|
||||
|
||||
url = hd(String.split(subscription.callback, "?"))
|
||||
query = URI.parse(subscription.callback).query || ""
|
||||
params = Map.merge(params, URI.decode_query(query))
|
||||
|
||||
with {:ok, response} <- getter.(url, [], params: params),
|
||||
^challenge <- response.body do
|
||||
changeset = Changeset.change(subscription, %{state: "active"})
|
||||
Repo.update(changeset)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't verify subscription")
|
||||
Logger.debug(inspect(e))
|
||||
{:error, subscription}
|
||||
end
|
||||
end
|
||||
|
||||
@supported_activities [
|
||||
"Create",
|
||||
"Follow",
|
||||
"Like",
|
||||
"Announce",
|
||||
"Undo",
|
||||
"Delete"
|
||||
]
|
||||
|
||||
def is_representable?(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in @supported_activities,
|
||||
do: Visibility.is_public?(activity)
|
||||
|
||||
def is_representable?(_), do: false
|
||||
|
||||
def publish(topic, user, %{data: %{"type" => type}} = activity)
|
||||
when type in @supported_activities do
|
||||
response =
|
||||
user
|
||||
|> FeedRepresenter.to_simple_form([activity], [user])
|
||||
|> :xmerl.export_simple(:xmerl_xml)
|
||||
|> to_string
|
||||
|
||||
query =
|
||||
from(
|
||||
sub in WebsubServerSubscription,
|
||||
where: sub.topic == ^topic and sub.state == "active",
|
||||
where: fragment("? > (NOW() at time zone 'UTC')", sub.valid_until)
|
||||
)
|
||||
|
||||
subscriptions = Repo.all(query)
|
||||
|
||||
callbacks = Enum.map(subscriptions, & &1.callback)
|
||||
reachable_callbacks_metadata = Instances.filter_reachable(callbacks)
|
||||
reachable_callbacks = Map.keys(reachable_callbacks_metadata)
|
||||
|
||||
subscriptions
|
||||
|> Enum.filter(&(&1.callback in reachable_callbacks))
|
||||
|> Enum.each(fn sub ->
|
||||
data = %{
|
||||
xml: response,
|
||||
topic: topic,
|
||||
callback: sub.callback,
|
||||
secret: sub.secret,
|
||||
unreachable_since: reachable_callbacks_metadata[sub.callback]
|
||||
}
|
||||
|
||||
Publisher.enqueue_one(__MODULE__, data)
|
||||
end)
|
||||
end
|
||||
|
||||
def publish(_, _, _), do: ""
|
||||
|
||||
def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
||||
|
||||
def sign(secret, doc) do
|
||||
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
|
||||
end
|
||||
|
||||
def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
|
||||
with {:ok, topic} <- valid_topic(params, user),
|
||||
{:ok, lease_time} <- lease_time(params),
|
||||
secret <- params["hub.secret"],
|
||||
callback <- params["hub.callback"] do
|
||||
subscription = get_subscription(topic, callback)
|
||||
|
||||
data = %{
|
||||
state: subscription.state || "requested",
|
||||
topic: topic,
|
||||
secret: secret,
|
||||
callback: callback
|
||||
}
|
||||
|
||||
change = Changeset.change(subscription, data)
|
||||
websub = Repo.insert_or_update!(change)
|
||||
|
||||
change =
|
||||
Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
|
||||
|
||||
websub = Repo.update!(change)
|
||||
|
||||
Federator.verify_websub(websub)
|
||||
|
||||
{:ok, websub}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.debug("Couldn't create subscription")
|
||||
Logger.debug(inspect(reason))
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def incoming_subscription_request(user, params) do
|
||||
Logger.info("Unhandled WebSub request for #{user.nickname}: #{inspect(params)}")
|
||||
|
||||
{:error, "Invalid WebSub request"}
|
||||
end
|
||||
|
||||
defp get_subscription(topic, callback) do
|
||||
Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
|
||||
%WebsubServerSubscription{}
|
||||
end
|
||||
|
||||
# Temp hack for mastodon.
|
||||
defp lease_time(%{"hub.lease_seconds" => ""}) do
|
||||
# three days
|
||||
{:ok, 60 * 60 * 24 * 3}
|
||||
end
|
||||
|
||||
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
|
||||
{:ok, String.to_integer(lease_seconds)}
|
||||
end
|
||||
|
||||
defp lease_time(_) do
|
||||
# three days
|
||||
{:ok, 60 * 60 * 24 * 3}
|
||||
end
|
||||
|
||||
defp valid_topic(%{"hub.topic" => topic}, user) do
|
||||
if topic == OStatus.feed_path(user) do
|
||||
{:ok, OStatus.feed_path(user)}
|
||||
else
|
||||
{:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
|
||||
end
|
||||
end
|
||||
|
||||
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
|
||||
topic = subscribed.info.topic
|
||||
# FIXME: Race condition, use transactions
|
||||
{:ok, subscription} =
|
||||
with subscription when not is_nil(subscription) <-
|
||||
Repo.get_by(WebsubClientSubscription, topic: topic) do
|
||||
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq()
|
||||
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
|
||||
Repo.update(change)
|
||||
else
|
||||
_e ->
|
||||
subscription = %WebsubClientSubscription{
|
||||
topic: topic,
|
||||
hub: subscribed.info.hub,
|
||||
subscribers: [subscriber.ap_id],
|
||||
state: "requested",
|
||||
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
|
||||
user: subscribed
|
||||
}
|
||||
|
||||
Repo.insert(subscription)
|
||||
end
|
||||
|
||||
requester.(subscription)
|
||||
end
|
||||
|
||||
def gather_feed_data(topic, getter \\ &HTTP.get/1) do
|
||||
with {:ok, response} <- getter.(topic),
|
||||
status when status in 200..299 <- response.status,
|
||||
body <- response.body,
|
||||
doc <- XML.parse_document(body),
|
||||
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
|
||||
hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do
|
||||
name = XML.string_from_xpath("/feed/author[1]/name", doc)
|
||||
preferred_username = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
|
||||
display_name = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
|
||||
avatar = OStatus.make_avatar_object(doc)
|
||||
bio = XML.string_from_xpath("/feed/author[1]/summary", doc)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
"uri" => uri,
|
||||
"hub" => hub,
|
||||
"nickname" => preferred_username || name,
|
||||
"name" => display_name || name,
|
||||
"host" => URI.parse(uri).host,
|
||||
"avatar" => avatar,
|
||||
"bio" => bio
|
||||
}}
|
||||
else
|
||||
e ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def request_subscription(websub, poster \\ &HTTP.post/3, timeout \\ 10_000) do
|
||||
data = [
|
||||
"hub.mode": "subscribe",
|
||||
"hub.topic": websub.topic,
|
||||
"hub.secret": websub.secret,
|
||||
"hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id)
|
||||
]
|
||||
|
||||
# This checks once a second if we are confirmed yet
|
||||
websub_checker = fn ->
|
||||
helper = fn helper ->
|
||||
:timer.sleep(1000)
|
||||
websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
|
||||
if websub, do: websub, else: helper.(helper)
|
||||
end
|
||||
|
||||
helper.(helper)
|
||||
end
|
||||
|
||||
task = Task.async(websub_checker)
|
||||
|
||||
with {:ok, %{status: 202}} <-
|
||||
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
|
||||
{:ok, websub} <- Task.yield(task, timeout) do
|
||||
{:ok, websub}
|
||||
else
|
||||
e ->
|
||||
Task.shutdown(task)
|
||||
|
||||
change = Ecto.Changeset.change(websub, %{state: "rejected"})
|
||||
{:ok, websub} = Repo.update(change)
|
||||
|
||||
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
|
||||
Logger.debug(fn -> "error: #{inspect(e)}" end)
|
||||
|
||||
{:error, websub}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_subscriptions(delta \\ 60 * 60 * 24) do
|
||||
Logger.debug("Refreshing subscriptions")
|
||||
|
||||
cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta)
|
||||
|
||||
query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
|
||||
|
||||
subs = Repo.all(query)
|
||||
|
||||
Enum.each(subs, fn sub ->
|
||||
Federator.request_subscription(sub)
|
||||
end)
|
||||
end
|
||||
|
||||
def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = params) do
|
||||
signature = sign(secret || "", xml)
|
||||
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
|
||||
|
||||
with {:ok, %{status: code}} when code in 200..299 <-
|
||||
HTTP.post(
|
||||
callback,
|
||||
xml,
|
||||
[
|
||||
{"Content-Type", "application/atom+xml"},
|
||||
{"X-Hub-Signature", "sha1=#{signature}"}
|
||||
]
|
||||
) do
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(callback)
|
||||
|
||||
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
|
||||
{:ok, code}
|
||||
else
|
||||
{_post_result, response} ->
|
||||
unless params[:unreachable_since], do: Instances.set_reachable(callback)
|
||||
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(response)}" end)
|
||||
{:error, response}
|
||||
end
|
||||
end
|
||||
|
||||
def gather_webfinger_links(%User{} = user) do
|
||||
[
|
||||
%{
|
||||
"rel" => "http://schemas.google.com/g/2010#updates-from",
|
||||
"type" => "application/atom+xml",
|
||||
"href" => OStatus.feed_path(user)
|
||||
},
|
||||
%{
|
||||
"rel" => "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template" => OStatus.remote_follow_path()
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def gather_nodeinfo_protocol_names, do: ["ostatus"]
|
||||
end
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Websub.WebsubClientSubscription do
|
||||
use Ecto.Schema
|
||||
alias Pleroma.User
|
||||
|
||||
schema "websub_client_subscriptions" do
|
||||
field(:topic, :string)
|
||||
field(:secret, :string)
|
||||
field(:valid_until, :naive_datetime_usec)
|
||||
field(:state, :string)
|
||||
field(:subscribers, {:array, :string}, default: [])
|
||||
field(:hub, :string)
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Websub.WebsubController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.Websub
|
||||
alias Pleroma.Web.Websub.WebsubClientSubscription
|
||||
|
||||
require Logger
|
||||
|
||||
plug(
|
||||
Pleroma.Web.FederatingPlug
|
||||
when action in [
|
||||
:websub_subscription_request,
|
||||
:websub_subscription_confirmation,
|
||||
:websub_incoming
|
||||
]
|
||||
)
|
||||
|
||||
def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
|
||||
user = User.get_cached_by_nickname(nickname)
|
||||
|
||||
with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) do
|
||||
conn
|
||||
|> send_resp(202, "Accepted")
|
||||
else
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> send_resp(500, reason)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Extract this into the Websub module
|
||||
def websub_subscription_confirmation(
|
||||
conn,
|
||||
%{
|
||||
"id" => id,
|
||||
"hub.mode" => "subscribe",
|
||||
"hub.challenge" => challenge,
|
||||
"hub.topic" => topic
|
||||
} = params
|
||||
) do
|
||||
Logger.debug("Got WebSub confirmation")
|
||||
Logger.debug(inspect(params))
|
||||
|
||||
lease_seconds =
|
||||
if params["hub.lease_seconds"] do
|
||||
String.to_integer(params["hub.lease_seconds"])
|
||||
else
|
||||
# Guess 3 days
|
||||
60 * 60 * 24 * 3
|
||||
end
|
||||
|
||||
with %WebsubClientSubscription{} = websub <-
|
||||
Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do
|
||||
valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), lease_seconds)
|
||||
change = Ecto.Changeset.change(websub, %{state: "accepted", valid_until: valid_until})
|
||||
{:ok, _websub} = Repo.update(change)
|
||||
|
||||
conn
|
||||
|> send_resp(200, challenge)
|
||||
else
|
||||
_e ->
|
||||
conn
|
||||
|> send_resp(500, "Error")
|
||||
end
|
||||
end
|
||||
|
||||
def websub_subscription_confirmation(conn, params) do
|
||||
Logger.info("Invalid WebSub confirmation request: #{inspect(params)}")
|
||||
|
||||
conn
|
||||
|> send_resp(500, "Invalid parameters")
|
||||
end
|
||||
|
||||
def websub_incoming(conn, %{"id" => id}) do
|
||||
with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")),
|
||||
signature <- String.downcase(signature),
|
||||
%WebsubClientSubscription{} = websub <- Repo.get(WebsubClientSubscription, id),
|
||||
{:ok, body, _conn} = read_body(conn),
|
||||
^signature <- Websub.sign(websub.secret, body) do
|
||||
Federator.incoming_doc(body)
|
||||
|
||||
conn
|
||||
|> send_resp(200, "OK")
|
||||
else
|
||||
_e ->
|
||||
Logger.debug("Can't handle incoming subscription post")
|
||||
|
||||
conn
|
||||
|> send_resp(500, "Error")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Websub.WebsubServerSubscription do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "websub_server_subscriptions" do
|
||||
field(:topic, :string)
|
||||
field(:callback, :string)
|
||||
field(:secret, :string)
|
||||
field(:valid_until, :naive_datetime)
|
||||
field(:state, :string)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
|
|
@ -8,10 +8,6 @@ defmodule Pleroma.Workers.ReceiverWorker do
|
|||
use Pleroma.Workers.WorkerHelper, queue: "federator_incoming"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%{"op" => "incoming_doc", "body" => doc}, _job) do
|
||||
Federator.perform(:incoming_doc, doc)
|
||||
end
|
||||
|
||||
def perform(%{"op" => "incoming_ap_doc", "params" => params}, _job) do
|
||||
Federator.perform(:incoming_ap_doc, params)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.SubscriberWorker do
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.Websub
|
||||
|
||||
use Pleroma.Workers.WorkerHelper, queue: "federator_outgoing"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%{"op" => "refresh_subscriptions"}, _job) do
|
||||
Federator.perform(:refresh_subscriptions)
|
||||
end
|
||||
|
||||
def perform(%{"op" => "request_subscription", "websub_id" => websub_id}, _job) do
|
||||
websub = Repo.get(Websub.WebsubClientSubscription, websub_id)
|
||||
Federator.perform(:request_subscription, websub)
|
||||
end
|
||||
|
||||
def perform(%{"op" => "verify_websub", "websub_id" => websub_id}, _job) do
|
||||
websub = Repo.get(Websub.WebsubServerSubscription, websub_id)
|
||||
Federator.perform(:verify_websub, websub)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue