Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85829766
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/mix/tasks/pleroma/activity.ex b/lib/mix/tasks/pleroma/activity.ex
index 512897a02..53323efd6 100644
--- a/lib/mix/tasks/pleroma/activity.ex
+++ b/lib/mix/tasks/pleroma/activity.ex
@@ -1,62 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Activity do
alias Pleroma.Activity
alias Pleroma.Activity.Search
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Pagination
require Logger
import Mix.Pleroma
import Ecto.Query
@shortdoc "A collection of activity debug tasks"
@moduledoc """
A collection of activity related tasks
mix pleroma.activity get <id>
"""
def run(["get", id | _rest]) do
start_pleroma()
id
|> Activity.get_by_id()
|> IO.inspect()
end
def run(["delete_by_keyword", user, keyword | _rest]) do
start_pleroma()
u = User.get_by_nickname(user)
+
Activity
- |> Activity.with_preloaded_object()
- |> Activity.restrict_deactivated_users()
- |> Activity.Queries.by_author(u)
- |> query_with(keyword)
- |> Pagination.fetch_paginated(
- %{"offset" => 0, "limit" => 20, "skip_order" => false},
- :offset
- )
- |> Enum.map(fn x -> CommonAPI.delete(x.id, u) end)
- |> Enum.count
- |> IO.puts
+ |> Activity.with_preloaded_object()
+ |> Activity.restrict_deactivated_users()
+ |> Activity.Queries.by_author(u)
+ |> query_with(keyword)
+ |> Pagination.fetch_paginated(
+ %{"offset" => 0, "limit" => 20, "skip_order" => false},
+ :offset
+ )
+ |> Enum.map(fn x -> CommonAPI.delete(x.id, u) end)
+ |> Enum.count()
+ |> IO.puts()
end
defp query_with(q, search_query) do
%{rows: [[tsc]]} =
Ecto.Adapters.SQL.query!(
Pleroma.Repo,
"select current_setting('default_text_search_config')::regconfig::oid;"
)
from([a, o] in q,
where:
fragment(
"to_tsvector(?::oid::regconfig, ?->>'content') @@ websearch_to_tsquery(?)",
^tsc,
o.data,
^search_query
)
)
end
end
diff --git a/lib/mix/tasks/pleroma/search.ex b/lib/mix/tasks/pleroma/search.ex
index 4e8e480d4..25a277f88 100644
--- a/lib/mix/tasks/pleroma/search.ex
+++ b/lib/mix/tasks/pleroma/search.ex
@@ -1,54 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Search do
use Mix.Task
import Mix.Pleroma
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Pagination
@shortdoc "Manages elasticsearch"
+ def run(["import_since", d | _rest]) do
+ start_pleroma()
+ {:ok, since, _} = DateTime.from_iso8601(d)
+
+ from(a in Activity, where: not ilike(a.actor, "%/relay") and a.inserted_at > ^since)
+ |> Activity.with_preloaded_object()
+ |> Activity.with_preloaded_user_actor()
+ |> get_all
+ end
+
def run(["import" | _rest]) do
start_pleroma()
from(a in Activity, where: not ilike(a.actor, "%/relay"))
|> where([a], fragment("(? ->> 'type'::text) = 'Create'", a.data))
|> Activity.with_preloaded_object()
|> Activity.with_preloaded_user_actor()
|> get_all
end
defp get_all(query, max_id \\ nil) do
IO.puts(max_id)
params = %{limit: 2000}
params =
if max_id == nil do
params
else
Map.put(params, :max_id, max_id)
end
res =
query
|> Pagination.fetch_paginated(params)
if res == [] do
:ok
else
res
- |> Enum.filter(fn x ->
- t = x.object
- |> Map.get(:data, %{})
- |> Map.get("type", "")
- t == "Note"
+ |> Enum.filter(fn x ->
+ t =
+ x.object
+ |> Map.get(:data, %{})
+ |> Map.get("type", "")
+
+ t == "Note"
end)
|> Pleroma.Elasticsearch.bulk_post(:activities)
get_all(query, List.last(res).id)
end
end
end
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 9bda7b485..6c26e0e95 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -1,520 +1,523 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Mix.Pleroma
alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
def run(["new", nickname, email | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [
name: :string,
bio: :string,
password: :string,
moderator: :boolean,
admin: :boolean,
assume_yes: :boolean
],
aliases: [
y: :assume_yes
]
)
name = Keyword.get(options, :name, nickname)
bio = Keyword.get(options, :bio, "")
{password, generated_password?} =
case Keyword.get(options, :password) do
nil ->
{:crypto.strong_rand_bytes(16) |> Base.encode64(), true}
password ->
{password, false}
end
moderator? = Keyword.get(options, :moderator, false)
admin? = Keyword.get(options, :admin, false)
assume_yes? = Keyword.get(options, :assume_yes, false)
shell_info("""
A user will be created with the following information:
- nickname: #{nickname}
- email: #{email}
- password: #{if(generated_password?, do: "[generated; a reset link will be created]", else: password)}
- name: #{name}
- bio: #{bio}
- moderator: #{if(moderator?, do: "true", else: "false")}
- admin: #{if(admin?, do: "true", else: "false")}
""")
proceed? = assume_yes? or shell_prompt("Continue?", "n") in ~w(Yn Y y)
if proceed? do
start_pleroma()
params = %{
nickname: nickname,
email: email,
password: password,
password_confirmation: password,
name: name,
bio: bio
}
changeset = User.register_changeset(%User{}, params, is_confirmed: true)
{:ok, _user} = User.register(changeset)
shell_info("User #{nickname} created")
if moderator? do
run(["set", nickname, "--moderator"])
end
if admin? do
run(["set", nickname, "--admin"])
end
if generated_password? do
run(["reset_password", nickname])
end
else
shell_info("User will not be created.")
end
end
def run(["rm", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.")
else
_ -> shell_error("No local user #{nickname}")
end
end
def run(["reset_password", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
shell_info("Generated password reset token for #{user.nickname}")
IO.puts("URL: #{Pleroma.Web.Router.Helpers.reset_password_url(Pleroma.Web.Endpoint,
:reset,
token.token)}")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["reset_mfa", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, _token} <- Pleroma.MFA.disable(user) do
shell_info("Multi-Factor Authentication disabled for #{user.nickname}")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["activate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname),
false <- user.is_active do
User.set_activation(user, true)
:timer.sleep(500)
shell_info("Successfully activated #{nickname}")
else
true ->
shell_info("User #{nickname} already activated")
_ ->
shell_error("No user #{nickname}")
end
end
def run(["deactivate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname),
true <- user.is_active do
User.set_activation(user, false)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully deactivated #{nickname} and unsubscribed all local followers")
end
else
false ->
shell_info("User #{nickname} already deactivated")
_ ->
shell_error("No user #{nickname}")
end
end
def run(["deactivate_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["deactivate", user.nickname])
end)
end)
|> Stream.run()
end
def run(["set", nickname | rest]) do
start_pleroma()
{options, [], []} =
OptionParser.parse(
rest,
strict: [
admin: :boolean,
confirmed: :boolean,
locked: :boolean,
moderator: :boolean
]
)
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user =
case Keyword.get(options, :admin) do
nil -> user
value -> set_admin(user, value)
end
user =
case Keyword.get(options, :confirmed) do
nil -> user
value -> set_confirmation(user, value)
end
user =
case Keyword.get(options, :locked) do
nil -> user
value -> set_locked(user, value)
end
_user =
case Keyword.get(options, :moderator) do
nil -> user
value -> set_moderator(user, value)
end
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["tag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["untag", nickname | tags]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags)
shell_info("Tags of #{user.nickname}: #{inspect(user.tags)}")
else
_ ->
shell_error("Could not change user tags for #{nickname}")
end
end
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
strict: [
expires_at: :string,
max_use: :integer
]
)
options =
options
|> Keyword.update(:expires_at, {:ok, nil}, fn
nil -> {:ok, nil}
val -> Date.from_iso8601(val)
end)
|> Enum.into(%{})
start_pleroma()
with {:ok, val} <- options[:expires_at],
options = Map.put(options, :expires_at, val),
{:ok, invite} <- UserInviteToken.create_invite(options) do
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
invite.token
)
IO.puts(url)
else
error ->
shell_error("Could not create invite token: #{inspect(error)}")
end
end
def run(["invites"]) do
start_pleroma()
shell_info("Invites list:")
UserInviteToken.list_invites()
|> Enum.each(fn invite ->
expire_info =
with expires_at when not is_nil(expires_at) <- invite.expires_at do
" | Expires at: #{Date.to_string(expires_at)}"
end
using_info =
with max_use when not is_nil(max_use) <- invite.max_use do
" | Max use: #{max_use} Left use: #{max_use - invite.uses}"
end
shell_info(
"ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{invite.used}#{expire_info}#{using_info}"
)
end)
end
def run(["revoke_invite", token]) do
start_pleroma()
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do
shell_info("Invite for token #{token} was revoked.")
else
_ -> shell_error("No invite found with token #{token}")
end
end
def run(["delete_activities", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user)
shell_info("User #{nickname} statuses deleted.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["change_email", nickname, email]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user
|> User.update_changeset(%{"email" => email})
|> User.update_and_set_cache()
shell_info("#{nickname}'s email updated")
end
end
def run(["show", nickname]) do
start_pleroma()
nickname
|> User.get_cached_by_nickname()
|> IO.inspect()
end
def run(["send_confirmation", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> IO.inspect()
|> Pleroma.Emails.Mailer.deliver!()
shell_info("#{nickname}'s email sent")
end
end
def run(["confirm", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.confirm(user)
message = if !user.is_confirmed, do: "needs", else: "doesn't need"
shell_info("#{nickname} #{message} confirmation.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["confirm_all"]) do
start_pleroma()
Pleroma.User.Query.build(%{
local: true,
is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user -> User.set_confirmation(user, true) end)
end)
|> Stream.run()
end
def run(["unconfirm_all"]) do
start_pleroma()
Pleroma.User.Query.build(%{
local: true,
is_active: true,
is_moderator: false,
is_admin: false,
invisible: false
})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user -> User.set_confirmation(user, false) end)
end)
|> Stream.run()
end
def run(["sign_out", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.global_sign_out(user)
shell_info("#{nickname} signed out from all apps.")
else
_ ->
shell_error("No local user #{nickname}")
end
end
def run(["blocking", nickname]) do
start_pleroma()
+
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
blocks = User.following_ap_ids(user)
IO.inspect(blocks, limit: :infinity)
end
end
def run(["timeline_query", nickname]) do
start_pleroma()
- params = %{ local: true }
+ params = %{local: true}
+
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
params =
params
|> Map.put(:type, ["Create", "Announce"])
- |> Map.put(:limit, 20)
+ |> Map.put(:limit, 20)
|> Map.put(:blocking_user, user)
|> Map.put(:muting_user, user)
|> Map.put(:reply_filtering_user, user)
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:local_only, params[:local])
|> Map.delete(:local)
+
_activities =
- [user.ap_id | User.following(user)]
- |> ActivityPub.fetch_activities(params)
+ [user.ap_id | User.following(user)]
+ |> ActivityPub.fetch_activities(params)
end
end
def run(["list"]) do
start_pleroma()
Pleroma.User.Query.build(%{local: true})
|> Pleroma.Repo.chunk_stream(500, :batches)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
shell_info(
"#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{user.is_locked}, is_active: #{user.is_active}"
)
end)
end)
|> Stream.run()
end
defp set_moderator(user, value) do
{:ok, user} =
user
|> Changeset.change(%{is_moderator: value})
|> User.update_and_set_cache()
shell_info("Moderator status of #{user.nickname}: #{user.is_moderator}")
user
end
defp set_admin(user, value) do
{:ok, user} = User.admin_api_update(user, %{is_admin: value})
shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user
end
defp set_locked(user, value) do
{:ok, user} =
user
|> Changeset.change(%{is_locked: value})
|> User.update_and_set_cache()
shell_info("Locked status of #{user.nickname}: #{user.is_locked}")
user
end
defp set_confirmation(user, value) do
{:ok, user} = User.set_confirmation(user, value)
shell_info("Confirmation status of #{user.nickname}: #{user.is_confirmed}")
user
end
end
diff --git a/lib/pleroma/elasticsearch/store.ex b/lib/pleroma/elasticsearch/store.ex
index f309bf7a5..68e14bb64 100644
--- a/lib/pleroma/elasticsearch/store.ex
+++ b/lib/pleroma/elasticsearch/store.ex
@@ -1,71 +1,73 @@
defmodule Pleroma.Elasticsearch do
alias Pleroma.Activity
alias Pleroma.Elasticsearch.DocumentMappings
alias Pleroma.Config
defp url do
Config.get([:elasticsearch, :url])
end
def put_by_id(id) do
id
|> Activity.get_by_id_with_object()
|> maybe_put_into_elasticsearch()
end
def maybe_put_into_elasticsearch({:ok, activity}) do
maybe_put_into_elasticsearch(activity)
end
- def maybe_put_into_elasticsearch(%{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity) do
+ def maybe_put_into_elasticsearch(
+ %{data: %{"type" => "Create"}, object: %{data: %{"type" => "Note"}}} = activity
+ ) do
if Config.get([:search, :provider]) == Pleroma.Search.Elasticsearch do
actor = Pleroma.Activity.user_actor(activity)
activity
|> Map.put(:user_actor, actor)
|> put()
end
end
def maybe_put_into_elasticsearch(_) do
{:ok, :skipped}
end
def put(%Activity{} = activity) do
Elastix.Document.index(
url(),
"activities",
"activity",
DocumentMappings.Activity.id(activity),
DocumentMappings.Activity.encode(activity)
)
end
def bulk_post(data, :activities) do
d =
data
|> Enum.map(fn d ->
[
%{index: %{_id: DocumentMappings.Activity.id(d)}},
DocumentMappings.Activity.encode(d)
]
end)
|> List.flatten()
Elastix.Bulk.post(
url(),
d,
index: "activities",
type: "activity"
)
end
def search_activities(q) do
Elastix.Search.search(
url(),
"activities",
["activity"],
q
)
end
end
diff --git a/lib/pleroma/search/elasticsearch.ex b/lib/pleroma/search/elasticsearch.ex
index 181009ad6..3815ef766 100644
--- a/lib/pleroma/search/elasticsearch.ex
+++ b/lib/pleroma/search/elasticsearch.ex
@@ -1,80 +1,87 @@
defmodule Pleroma.Search.Elasticsearch do
@behaviour Pleroma.Search
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.ActivityPub.Visibility
defp to_es(term) when is_binary(term) do
%{
match: %{
content: %{
query: term,
operator: "AND"
}
}
}
end
defp to_es({:quoted, term}), do: to_es(term)
defp to_es({:filter, ["hashtag", query]}) do
%{
term: %{
hashtags: %{
value: query
}
}
}
end
defp to_es({:filter, [field, query]}) do
%{
term: %{
field => %{
value: query
}
}
}
end
defp parse(query) do
query
|> SearchParser.parse!()
|> Enum.map(&to_es/1)
end
@impl Pleroma.Search
def search(%{assigns: %{user: user}} = _conn, %{q: query} = _params, _options) do
q = %{
+ size: 500,
+ terminate_after: 500,
+ timeout: "10s",
+ sort: [
+ %{"_timestamp" => "desc"}
+ ],
query: %{
bool: %{
must: parse(String.trim(query))
}
}
}
out = Pleroma.Elasticsearch.search_activities(q)
with {:ok, raw_results} <- out do
results =
raw_results
|> Map.get(:body, %{})
|> Map.get("hits", %{})
|> Map.get("hits", [])
|> Enum.map(fn result -> result["_id"] end)
|> Pleroma.Activity.all_by_ids_with_object()
- |> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
+ |> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
+ |> Enum.reverse()
%{
"accounts" => [],
"hashtags" => [],
"statuses" =>
StatusView.render("index.json",
activities: results,
for: user,
as: :activity
)
}
end
end
end
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index f6a131c21..3266b1c60 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -1,604 +1,606 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
require Logger
def block(blocker, blocked) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
{:ok, block}
end
end
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_content_length(content, !!maybe_attachment),
{_, {:ok, chat_message_data, _meta}} <-
{:build_object,
Builder.chat_message(
user,
recipient.ap_id,
content |> format_chat_content,
attachment: maybe_attachment
)},
{_, {:ok, create_activity_data, _meta}} <-
{:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(create_activity_data,
local: true,
idempotency_key: opts[:idempotency_key]
)} do
{:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
e -> e
end
end
defp format_chat_content(nil), do: nil
defp format_chat_content(content) do
{text, _, _} =
content
|> Formatter.html_escape("text/plain")
|> Formatter.linkify()
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
text
end
defp validate_chat_content_length(_, true), do: :ok
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
defp validate_chat_content_length(content, _) do
if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
:ok
else
{:error, :content_too_long}
end
end
def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
{:ok, unblock}
else
{:fetch_block, nil} ->
if User.blocks?(blocker, blocked) do
User.unblock(blocker, blocked)
{:ok, :no_activity}
else
{:error, :not_blocking}
end
e ->
e
end
end
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follow_data, _} <- Builder.follow(follower, followed),
{:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
if activity.data["state"] == "reject" do
{:error, :rejected}
else
{:ok, follower, followed, activity}
end
end
end
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower}
end
end
def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
{:ok, follower}
end
end
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
{:ok, follower}
end
end
def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id)},
{_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
{:find_activity, _} ->
{:error, :not_found}
{:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete(activity_id, user)
else
_ ->
Logger.error(
"Could not insert tombstone for missing object on deletion. Object is #{object}."
)
{:error, dgettext("errors", "Could not delete")}
end
_ ->
{:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public),
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
{:ok, activity}
else
{:existing_announce, %Activity{} = announce} ->
{:ok, announce}
_ ->
{:error, :not_found}
end
end
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
@spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
def favorite(%User{} = user, id) do
case favorite_helper(user, id) do
{:ok, _} = res ->
res
{:error, :not_found} = res ->
res
{:error, e} ->
Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not favorite")}
end
end
def favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
{:find_object, _} ->
{:error, :not_found}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
else
{:error, e}
end
e ->
{:error, e}
end
end
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
end
end
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not remove reaction emoji")}
end
end
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
{:ok, answer_object, _meta} =
Builder.answer(user, object, Enum.at(options, index)["name"])
{:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
{:ok, activity, _meta} =
activity_data
|> Map.put("cc", answer_object["cc"])
|> Map.put("context", answer_object["context"])
|> Pipeline.common_pipeline(local: true)
# TODO: Do preload of Pleroma.Object in Pipeline
Activity.normalize(activity.data)
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
defp validate_not_author(_, _), do: :ok
defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{:error, dgettext("errors", "Already voted")}
end
end
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
when is_list(any_of) and any_of != [],
do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
when is_list(one_of) and one_of != [],
do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end
def public_announce?(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
def public_announce?(object, _) do
Visibility.is_public?(object)
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
end
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity, fetch: false) do
Visibility.get_visibility(object)
end
end
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
expiry = DateTime.add(DateTime.utc_now(), in_seconds)
if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
{:ok, expiry}
else
{:error, "Expiry date is too soon"}
end
end
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
def listen(user, data) do
with {:ok, draft} <- ActivityDraft.listen(user, data) do
ActivityPub.listen(draft.changes)
end
end
def post(user, %{status: _} = data) do
with {:ok, draft} <- ActivityDraft.create(user, data) do
activity = ActivityPub.create(draft.changes, draft.preview?)
+
unless draft.preview? do
Pleroma.Elasticsearch.maybe_put_into_elasticsearch(activity)
end
+
activity
end
end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id),
true <- object_type_is_allowed_for_pin(activity.object),
true <- activity_is_public(activity),
{:ok, pin_data, _} <- Builder.pin(user, activity.object),
{:ok, _pin, _} <-
Pipeline.common_pipeline(pin_data,
local: true,
activity_id: id
) do
{:ok, activity}
else
{:error, {:side_effects, error}} -> error
error -> error
end
end
defp create_activity_by_id(id) do
with nil <- Activity.create_by_id_with_object(id) do
{:error, :not_found}
end
end
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed}
end
end
defp activity_is_public(activity) do
with false <- Visibility.is_public?(activity) do
{:error, :visibility_error}
end
end
@spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
{:ok, _unpin, _} <-
Pipeline.common_pipeline(unpin_data,
local: true,
activity_id: activity.id,
expires_at: activity.data["expires_at"],
featured_address: user.featured_address
) do
{:ok, activity}
end
end
def add_mute(user, activity, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_conversation",
%{"user_id" => user.id, "activity_id" => activity.id},
schedule_in: expires_in
)
end
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
def remove_mute(%User{} = user, %Activity{} = activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
def remove_mute(user_id, activity_id) do
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
remove_mute(user, activity)
else
{what, result} = error ->
Logger.warn(
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
)
{:error, error}
end
end
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
when is_binary(context) do
ThreadMute.exists?(user_id, context)
end
def thread_muted?(_, _), do: false
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: Map.get(data, :forward, false)
})
end
end
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
end
end
defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{visibility: visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(%User{} = user, %User{} = target) do
UserRelationship.create_reblog_mute(user, target)
end
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
end
def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
fake_record_fallback ->
# TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)
true ->
nil
end
end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 196d0006b..0cb6aff16 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -1,214 +1,213 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma
require Pleroma.Constants
alias Pleroma.Config
socket("/socket", Pleroma.Web.UserSocket)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Pleroma.Web.Plugs.SetLocalePlug)
plug(CORSPlug)
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
# Cache-control headers are duplicated in case we turn off etags in the future
plug(
Pleroma.Web.Plugs.InstanceStatic,
at: "/",
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
cache_control_for_etags: "public, max-age=1209600",
headers: %{
"cache-control" => "public, max-age=1209600"
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
# Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/pleroma/fedife/")
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/pleroma/fedife",
frontend_type: :fedife,
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
-
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/",
from: :pleroma,
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static,
at: "/pleroma/admin/",
from: {:pleroma, "priv/static/adminfe/"}
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug(Phoenix.CodeReloader)
end
plug(Pleroma.Web.Plugs.TrailingFormatPlug)
plug(Plug.RequestId)
plug(Plug.Logger, log: :debug)
plug(Plug.Parsers,
parsers: [
:urlencoded,
{:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
:json
],
pass: ["*/*"],
json_decoder: Jason,
length: Config.get([:instance, :upload_limit]),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
plug(Plug.MethodOverride)
plug(Plug.Head)
secure_cookies = Config.get([__MODULE__, :secure_cookie_flag])
cookie_name =
if secure_cookies,
do: "__Host-pleroma_key",
else: "pleroma_key"
extra =
Config.get([__MODULE__, :extra_cookie_attrs])
|> Enum.join(";")
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
plug(
Plug.Session,
store: :cookie,
key: cookie_name,
signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
http_only: true,
secure: secure_cookies,
extra: extra
)
plug(Pleroma.Web.Plugs.RemoteIp)
defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter
end
defmodule PipelineInstrumenter do
use Prometheus.PlugPipelineInstrumenter
end
defmodule MetricsExporter do
use Prometheus.PlugExporter
end
defmodule MetricsExporterCaller do
@behaviour Plug
def init(opts), do: opts
def call(conn, opts) do
prometheus_config = Application.get_env(:prometheus, MetricsExporter, [])
ip_whitelist = List.wrap(prometheus_config[:ip_whitelist])
cond do
!prometheus_config[:enabled] ->
conn
ip_whitelist != [] and
!Enum.find(ip_whitelist, fn ip ->
Pleroma.Helpers.InetHelper.parse_address(ip) == {:ok, conn.remote_ip}
end) ->
conn
true ->
MetricsExporter.call(conn, opts)
end
end
end
plug(PipelineInstrumenter)
plug(MetricsExporterCaller)
plug(Pleroma.Web.Router)
@doc """
Dynamically loads configuration from the system environment
on startup.
It receives the endpoint configuration from the config files
and must return the updated configuration.
"""
def load_from_system_env(config) do
port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
end
def websocket_url do
String.replace_leading(url(), "http", "ws")
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 12:15 AM (13 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801955
Default Alt Text
(48 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline