summaryrefslogtreecommitdiff
path: root/lib/pleroma
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma')
-rw-r--r--lib/pleroma/announcement.ex160
-rw-r--r--lib/pleroma/announcement_read_relationship.ex55
-rw-r--r--lib/pleroma/application_requirements.ex3
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex40
-rw-r--r--lib/pleroma/constants.ex6
-rw-r--r--lib/pleroma/docs/translator.ex10
-rw-r--r--lib/pleroma/docs/translator/compiler.ex119
-rw-r--r--lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex25
-rw-r--r--lib/pleroma/emoji.ex14
-rw-r--r--lib/pleroma/emoji/combinations.ex45
-rw-r--r--lib/pleroma/following_relationship.ex3
-rw-r--r--lib/pleroma/http/adapter_helper/hackney.ex4
-rw-r--r--lib/pleroma/reverse_proxy/client/hackney.ex1
-rw-r--r--lib/pleroma/upload.ex27
-rw-r--r--lib/pleroma/upload/filter/exiftool/read_description.ex49
-rw-r--r--lib/pleroma/upload/filter/exiftool/strip_location.ex (renamed from lib/pleroma/upload/filter/exiftool.ex)2
-rw-r--r--lib/pleroma/user.ex98
-rw-r--r--lib/pleroma/user/backup.ex18
-rw-r--r--lib/pleroma/user_relationship.ex39
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex3
-rw-r--r--lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex21
-rw-r--r--lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex18
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex6
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex12
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex18
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex6
-rw-r--r--lib/pleroma/web/admin_api/controllers/announcement_controller.ex83
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex50
-rw-r--r--lib/pleroma/web/admin_api/views/announcement_view.ex15
-rw-r--r--lib/pleroma/web/api_spec/operations/account_operation.ex39
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex165
-rw-r--r--lib/pleroma/web/api_spec/operations/announcement_operation.ex57
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex6
-rw-r--r--lib/pleroma/web/api_spec/operations/twitter_util_operation.ex140
-rw-r--r--lib/pleroma/web/api_spec/schemas/account.ex1
-rw-r--r--lib/pleroma/web/api_spec/schemas/announcement.ex45
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex60
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex11
-rw-r--r--lib/pleroma/web/mastodon_api/views/announcement_view.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex1
-rw-r--r--lib/pleroma/web/router.ex19
-rw-r--r--lib/pleroma/web/static_fe/static_fe_controller.ex9
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex99
-rw-r--r--lib/pleroma/web/twitter_api/views/remote_follow_view.ex6
-rw-r--r--lib/pleroma/workers/backup_worker.ex24
48 files changed, 1554 insertions, 112 deletions
diff --git a/lib/pleroma/announcement.ex b/lib/pleroma/announcement.ex
new file mode 100644
index 000000000..d97c5e728
--- /dev/null
+++ b/lib/pleroma/announcement.ex
@@ -0,0 +1,160 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Announcement do
+ use Ecto.Schema
+
+ import Ecto.Changeset, only: [cast: 3, validate_required: 2]
+ import Ecto.Query
+
+ alias Pleroma.AnnouncementReadRelationship
+ alias Pleroma.Repo
+
+ @type t :: %__MODULE__{}
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "announcements" do
+ field(:data, :map)
+ field(:starts_at, :utc_datetime)
+ field(:ends_at, :utc_datetime)
+ field(:rendered, :map)
+
+ timestamps(type: :utc_datetime)
+ end
+
+ def change(struct, params \\ %{}) do
+ struct
+ |> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered])
+ |> validate_required([:data])
+ end
+
+ defp validate_params(struct, params) do
+ base_data =
+ %{
+ "content" => "",
+ "all_day" => false
+ }
+ |> Map.merge((struct && struct.data) || %{})
+
+ merged_data =
+ Map.merge(base_data, params.data)
+ |> Map.take(["content", "all_day"])
+
+ params
+ |> Map.merge(%{data: merged_data})
+ |> add_rendered_properties()
+ end
+
+ def add_rendered_properties(params) do
+ {content_html, _, _} =
+ Pleroma.Web.CommonAPI.Utils.format_input(params.data["content"], "text/plain",
+ mentions_format: :full
+ )
+
+ rendered = %{
+ "content" => content_html
+ }
+
+ params
+ |> Map.put(:rendered, rendered)
+ end
+
+ def add(params) do
+ changeset = change(%__MODULE__{}, params)
+
+ Repo.insert(changeset)
+ end
+
+ def update(announcement, params) do
+ changeset = change(announcement, params)
+
+ Repo.update(changeset)
+ end
+
+ def list_all do
+ __MODULE__
+ |> Repo.all()
+ end
+
+ def list_paginated(%{limit: limited_number, offset: offset_number}) do
+ __MODULE__
+ |> limit(^limited_number)
+ |> offset(^offset_number)
+ |> Repo.all()
+ end
+
+ def get_by_id(id) do
+ Repo.get_by(__MODULE__, id: id)
+ end
+
+ def delete_by_id(id) do
+ with announcement when not is_nil(announcement) <- get_by_id(id),
+ {:ok, _} <- Repo.delete(announcement) do
+ :ok
+ else
+ _ ->
+ :error
+ end
+ end
+
+ def read_by?(announcement, user) do
+ AnnouncementReadRelationship.exists?(user, announcement)
+ end
+
+ def mark_read_by(announcement, user) do
+ AnnouncementReadRelationship.mark_read(user, announcement)
+ end
+
+ def render_json(announcement, opts \\ []) do
+ extra_params =
+ case Keyword.fetch(opts, :for) do
+ {:ok, user} when not is_nil(user) ->
+ %{read: read_by?(announcement, user)}
+
+ _ ->
+ %{}
+ end
+
+ admin_extra_params =
+ case Keyword.fetch(opts, :admin) do
+ {:ok, true} ->
+ %{pleroma: %{raw_content: announcement.data["content"]}}
+
+ _ ->
+ %{}
+ end
+
+ base = %{
+ id: announcement.id,
+ content: announcement.rendered["content"],
+ starts_at: announcement.starts_at,
+ ends_at: announcement.ends_at,
+ all_day: announcement.data["all_day"],
+ published_at: announcement.inserted_at,
+ updated_at: announcement.updated_at,
+ mentions: [],
+ statuses: [],
+ tags: [],
+ emojis: [],
+ reactions: []
+ }
+
+ base
+ |> Map.merge(extra_params)
+ |> Map.merge(admin_extra_params)
+ end
+
+ # "visible" means:
+ # starts_at < time < ends_at
+ def list_all_visible_when(time) do
+ __MODULE__
+ |> where([a], is_nil(a.starts_at) or a.starts_at < ^time)
+ |> where([a], is_nil(a.ends_at) or a.ends_at > ^time)
+ |> Repo.all()
+ end
+
+ def list_all_visible do
+ list_all_visible_when(DateTime.now("Etc/UTC") |> elem(1))
+ end
+end
diff --git a/lib/pleroma/announcement_read_relationship.ex b/lib/pleroma/announcement_read_relationship.ex
new file mode 100644
index 000000000..9b64404ce
--- /dev/null
+++ b/lib/pleroma/announcement_read_relationship.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.AnnouncementReadRelationship do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias FlakeId.Ecto.CompatType
+ alias Pleroma.Announcement
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+
+ schema "announcement_read_relationships" do
+ belongs_to(:user, User, type: CompatType)
+ belongs_to(:announcement, Announcement, type: CompatType)
+
+ timestamps(updated_at: false)
+ end
+
+ def mark_read(user, announcement) do
+ %__MODULE__{}
+ |> cast(%{user_id: user.id, announcement_id: announcement.id}, [:user_id, :announcement_id])
+ |> validate_required([:user_id, :announcement_id])
+ |> foreign_key_constraint(:user_id)
+ |> foreign_key_constraint(:announcement_id)
+ |> unique_constraint([:user_id, :announcement_id])
+ |> Repo.insert()
+ end
+
+ def mark_unread(user, announcement) do
+ with relationship <- get(user, announcement),
+ {:exists, true} <- {:exists, not is_nil(relationship)},
+ {:ok, _} <- Repo.delete(relationship) do
+ :ok
+ else
+ {:exists, false} ->
+ :ok
+
+ _ ->
+ :error
+ end
+ end
+
+ def get(user, announcement) do
+ Repo.get_by(__MODULE__, user_id: user.id, announcement_id: announcement.id)
+ end
+
+ def exists?(user, announcement) do
+ not is_nil(get(user, announcement))
+ end
+end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
index 06d388694..44b1c1705 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application_requirements.ex
@@ -164,7 +164,8 @@ defmodule Pleroma.ApplicationRequirements do
defp check_system_commands!(:ok) do
filter_commands_statuses = [
- check_filter(Pleroma.Upload.Filter.Exiftool, "exiftool"),
+ check_filter(Pleroma.Upload.Filter.Exiftool.StripLocation, "exiftool"),
+ check_filter(Pleroma.Upload.Filter.Exiftool.ReadDescription, "exiftool"),
check_filter(Pleroma.Upload.Filter.Mogrify, "mogrify"),
check_filter(Pleroma.Upload.Filter.Mogrifun, "mogrify"),
check_filter(Pleroma.Upload.Filter.AnalyzeMetadata, "mogrify"),
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 118dd3acc..599f1d3cf 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -20,6 +20,43 @@ defmodule Pleroma.Config.DeprecationWarnings do
"\n* `config :pleroma, :instance, mrf_transparency_exclusions` is now `config :pleroma, :mrf, transparency_exclusions`"}
]
+ def check_exiftool_filter do
+ filters = Config.get([Pleroma.Upload]) |> Keyword.get(:filters, [])
+
+ if Pleroma.Upload.Filter.Exiftool in filters do
+ Logger.warn("""
+ !!!DEPRECATION WARNING!!!
+ Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
+
+ ```
+ config :pleroma, Pleroma.Upload,
+ filters: [Pleroma.Upload.Filter.Exiftool]
+ ```
+
+ Is now
+
+
+ ```
+ config :pleroma, Pleroma.Upload,
+ filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
+ ```
+ """)
+
+ new_config =
+ filters
+ |> Enum.map(fn
+ Pleroma.Upload.Filter.Exiftool -> Pleroma.Upload.Filter.Exiftool.StripLocation
+ filter -> filter
+ end)
+
+ Config.put([Pleroma.Upload, :filters], new_config)
+
+ :error
+ else
+ :ok
+ end
+ end
+
def check_simple_policy_tuples do
has_strings =
Config.get([:mrf_simple])
@@ -180,7 +217,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
- check_simple_policy_tuples()
+ check_simple_policy_tuples(),
+ check_exiftool_filter()
]
|> Enum.reduce(:ok, fn
:ok, :ok -> :ok
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index a42c71d23..7b63ab06e 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -27,4 +27,10 @@ defmodule Pleroma.Constants do
do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css)
)
+
+ # basic regex, just there to weed out potential mistakes
+ # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
+ const(mime_regex,
+ do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
+ )
end
diff --git a/lib/pleroma/docs/translator.ex b/lib/pleroma/docs/translator.ex
new file mode 100644
index 000000000..13e33c87e
--- /dev/null
+++ b/lib/pleroma/docs/translator.ex
@@ -0,0 +1,10 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Docs.Translator do
+ require Pleroma.Docs.Translator.Compiler
+ require Pleroma.Web.Gettext
+
+ @before_compile Pleroma.Docs.Translator.Compiler
+end
diff --git a/lib/pleroma/docs/translator/compiler.ex b/lib/pleroma/docs/translator/compiler.ex
new file mode 100644
index 000000000..5d27d9fa2
--- /dev/null
+++ b/lib/pleroma/docs/translator/compiler.ex
@@ -0,0 +1,119 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Docs.Translator.Compiler do
+ @external_resource "config/description.exs"
+ @raw_config Pleroma.Config.Loader.read("config/description.exs")
+ @raw_descriptions @raw_config[:pleroma][:config_description]
+
+ defmacro __before_compile__(_env) do
+ strings =
+ __MODULE__.descriptions()
+ |> __MODULE__.extract_strings()
+
+ quote do
+ def placeholder do
+ unquote do
+ Enum.map(
+ strings,
+ fn {path, type, string} ->
+ ctxt = msgctxt_for(path, type)
+
+ quote do
+ Pleroma.Web.Gettext.dpgettext_noop(
+ "config_descriptions",
+ unquote(ctxt),
+ unquote(string)
+ )
+ end
+ end
+ )
+ end
+ end
+ end
+ end
+
+ def descriptions do
+ Pleroma.Web.ActivityPub.MRF.config_descriptions()
+ |> Enum.reduce(@raw_descriptions, fn description, acc -> [description | acc] end)
+ |> Pleroma.Docs.Generator.convert_to_strings()
+ end
+
+ def extract_strings(descriptions) do
+ descriptions
+ |> Enum.reduce(%{strings: [], path: []}, &process_item/2)
+ |> Map.get(:strings)
+ end
+
+ defp process_item(entity, acc) do
+ current_level =
+ acc
+ |> process_desc(entity)
+ |> process_label(entity)
+
+ process_children(entity, current_level)
+ end
+
+ defp process_desc(acc, %{description: desc} = item) do
+ %{
+ strings: [{acc.path ++ [key_for(item)], "description", desc} | acc.strings],
+ path: acc.path
+ }
+ end
+
+ defp process_desc(acc, _) do
+ acc
+ end
+
+ defp process_label(acc, %{label: label} = item) do
+ %{
+ strings: [{acc.path ++ [key_for(item)], "label", label} | acc.strings],
+ path: acc.path
+ }
+ end
+
+ defp process_label(acc, _) do
+ acc
+ end
+
+ defp process_children(%{children: children} = item, acc) do
+ current_level = Map.put(acc, :path, acc.path ++ [key_for(item)])
+
+ children
+ |> Enum.reduce(current_level, &process_item/2)
+ |> Map.put(:path, acc.path)
+ end
+
+ defp process_children(_, acc) do
+ acc
+ end
+
+ def msgctxt_for(path, type) do
+ "config #{type} at #{Enum.join(path, " > ")}"
+ end
+
+ defp convert_group({_, group}) do
+ group
+ end
+
+ defp convert_group(group) do
+ group
+ end
+
+ def key_for(%{group: group, key: key}) do
+ "#{convert_group(group)}-#{key}"
+ end
+
+ def key_for(%{group: group}) do
+ convert_group(group)
+ end
+
+ def key_for(%{key: key}) do
+ key
+ end
+
+ def key_for(_) do
+ nil
+ end
+end
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex
new file mode 100644
index 000000000..31d51577d
--- /dev/null
+++ b/lib/pleroma/ecto_type/activity_pub/object_validators/mime.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.MIME do
+ use Ecto.Type
+
+ require Pleroma.Constants
+
+ def type, do: :string
+
+ def cast(mime) when is_binary(mime) do
+ if mime =~ Pleroma.Constants.mime_regex() do
+ {:ok, mime}
+ else
+ {:ok, "application/octet-stream"}
+ end
+ end
+
+ def cast(_), do: :error
+
+ def dump(data), do: {:ok, data}
+
+ def load(data), do: {:ok, data}
+end
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 35f0da816..dd65d56ae 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Emoji do
"""
use GenServer
+ alias Pleroma.Emoji.Combinations
alias Pleroma.Emoji.Loader
require Logger
@@ -137,4 +138,17 @@ defmodule Pleroma.Emoji do
end
def is_unicode_emoji?(_), do: false
+
+ emoji_qualification_map =
+ emojis
+ |> Enum.filter(&String.contains?(&1, "\uFE0F"))
+ |> Combinations.variate_emoji_qualification()
+
+ for {qualified, unqualified_list} <- emoji_qualification_map do
+ for unqualified <- unqualified_list do
+ def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
+ end
+ end
+
+ def fully_qualify_emoji(emoji), do: emoji
end
diff --git a/lib/pleroma/emoji/combinations.ex b/lib/pleroma/emoji/combinations.ex
new file mode 100644
index 000000000..981c73596
--- /dev/null
+++ b/lib/pleroma/emoji/combinations.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.Combinations do
+ # FE0F is the emoji variation sequence. It is used for fully-qualifying
+ # emoji, and that includes emoji combinations.
+ # This code generates combinations per emoji: for each FE0F, all possible
+ # combinations of the character being removed or staying will be generated.
+ # This is made as an attempt to find all partially-qualified and unqualified
+ # versions of a fully-qualified emoji.
+ # I have found *no cases* for which this would be a problem, after browsing
+ # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most
+ # likely sane too.
+
+ defp qualification_combinations(codepoints) do
+ qualification_combinations([[]], codepoints)
+ end
+
+ defp qualification_combinations(acc, []), do: acc
+
+ defp qualification_combinations(acc, ["\uFE0F" | tail]) do
+ acc
+ |> Enum.flat_map(fn x -> [x, x ++ ["\uFE0F"]] end)
+ |> qualification_combinations(tail)
+ end
+
+ defp qualification_combinations(acc, [codepoint | tail]) do
+ acc
+ |> Enum.map(&Kernel.++(&1, [codepoint]))
+ |> qualification_combinations(tail)
+ end
+
+ def variate_emoji_qualification(emoji) when is_binary(emoji) do
+ emoji
+ |> String.codepoints()
+ |> qualification_combinations()
+ |> Enum.map(&List.to_string/1)
+ end
+
+ def variate_emoji_qualification(emoji) when is_list(emoji) do
+ emoji
+ |> Enum.map(fn emoji -> {emoji, variate_emoji_qualification(emoji)} end)
+ end
+end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index e6449aa67..15664c876 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -194,12 +194,13 @@ defmodule Pleroma.FollowingRelationship do
|> join(:inner, [r], f in assoc(r, :follower))
|> where(following_id: ^origin.id)
|> where([r, f], f.allow_following_move == true)
+ |> where([r, f], f.local == true)
|> limit(50)
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
- Repo.delete(following_relationship)
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
+ Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
end)
|> case do
[] ->
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
index b4f2f0cc2..f3be1f3d0 100644
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ b/lib/pleroma/http/adapter_helper/hackney.ex
@@ -24,10 +24,6 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
end
- defp add_scheme_opts(opts, %URI{scheme: "https"}) do
- Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
- end
-
defp add_scheme_opts(opts, _), do: opts
defp maybe_add_with_body(opts) do
diff --git a/lib/pleroma/reverse_proxy/client/hackney.ex b/lib/pleroma/reverse_proxy/client/hackney.ex
index 41eaf06cc..d3e986912 100644
--- a/lib/pleroma/reverse_proxy/client/hackney.ex
+++ b/lib/pleroma/reverse_proxy/client/hackney.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
def request(method, url, headers, body, opts \\ []) do
- opts = Keyword.put(opts, :ssl_options, versions: [:"tlsv1.2", :"tlsv1.1", :tlsv1])
:hackney.request(method, url, headers, body, opts)
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 242813dcd..db2909276 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -60,12 +60,23 @@ defmodule Pleroma.Upload do
width: integer(),
height: integer(),
blurhash: String.t(),
+ description: String.t(),
path: String.t()
}
- defstruct [:id, :name, :tempfile, :content_type, :width, :height, :blurhash, :path]
-
- defp get_description(opts, upload) do
- case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
+ defstruct [
+ :id,
+ :name,
+ :tempfile,
+ :content_type,
+ :width,
+ :height,
+ :blurhash,
+ :description,
+ :path
+ ]
+
+ defp get_description(upload) do
+ case {upload.description, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
@@ -81,7 +92,7 @@ defmodule Pleroma.Upload do
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
- description = get_description(opts, upload),
+ description = get_description(upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
@@ -152,7 +163,8 @@ defmodule Pleroma.Upload do
id: UUID.generate(),
name: file.filename,
tempfile: file.path,
- content_type: file.content_type
+ content_type: file.content_type,
+ description: opts.description
}}
end
end
@@ -172,7 +184,8 @@ defmodule Pleroma.Upload do
id: UUID.generate(),
name: hash <> "." <> ext,
tempfile: tmp_path,
- content_type: content_type
+ content_type: content_type,
+ description: opts.description
}}
end
end
diff --git a/lib/pleroma/upload/filter/exiftool/read_description.ex b/lib/pleroma/upload/filter/exiftool/read_description.ex
new file mode 100644
index 000000000..03d698a81
--- /dev/null
+++ b/lib/pleroma/upload/filter/exiftool/read_description.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
+ @moduledoc """
+ Gets a valid description from the related EXIF tags and provides them in the response if no description is provided yet.
+ It will first check ImageDescription, when that doesn't probide a valid description, it will check iptc:Caption-Abstract.
+ A valid description means the fields are filled in and not too long (see `:instance, :description_limit`).
+ """
+ @behaviour Pleroma.Upload.Filter
+
+ @spec filter(Pleroma.Upload.t()) :: {:ok, any()} | {:error, String.t()}
+
+ def filter(%Pleroma.Upload{description: description})
+ when is_binary(description),
+ do: {:ok, :noop}
+
+ def filter(%Pleroma.Upload{tempfile: file} = upload),
+ do: {:ok, :filtered, upload |> Map.put(:description, read_description_from_exif_data(file))}
+
+ def filter(_, _), do: {:ok, :noop}
+
+ defp read_description_from_exif_data(file) do
+ nil
+ |> read_when_empty(file, "-ImageDescription")
+ |> read_when_empty(file, "-iptc:Caption-Abstract")
+ end
+
+ defp read_when_empty(current_description, _, _) when is_binary(current_description),
+ do: current_description
+
+ defp read_when_empty(_, file, tag) do
+ try do
+ {tag_content, 0} =
+ System.cmd("exiftool", ["-b", "-s3", tag, file], stderr_to_stdout: true, parallelism: true)
+
+ tag_content = String.trim(tag_content)
+
+ if tag_content != "" and
+ String.length(tag_content) <=
+ Pleroma.Config.get([:instance, :description_limit]),
+ do: tag_content,
+ else: nil
+ rescue
+ _ in ErlangError -> nil
+ end
+ end
+end
diff --git a/lib/pleroma/upload/filter/exiftool.ex b/lib/pleroma/upload/filter/exiftool/strip_location.ex
index 36cc045c2..6100527d3 100644
--- a/lib/pleroma/upload/filter/exiftool.ex
+++ b/lib/pleroma/upload/filter/exiftool/strip_location.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Upload.Filter.Exiftool do
+defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
@moduledoc """
Strips GPS related EXIF tags and overwrites the file in place.
Also strips or replaces filesystem metadata e.g., timestamps.
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 747a83e8d..eeea240fb 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -706,7 +706,7 @@ defmodule Pleroma.User do
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+ |> validate_not_restricted_nickname(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
@@ -754,17 +754,9 @@ defmodule Pleroma.User do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
- |> validate_change(:email, fn :email, email ->
- valid? =
- Config.get([User, :email_blacklist])
- |> Enum.all?(fn blacklisted_domain ->
- !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
- end)
-
- if valid?, do: [], else: [email: "Invalid email"]
- end)
+ |> validate_email_not_in_blacklisted_domain(:email)
|> unique_constraint(:nickname)
- |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
+ |> validate_not_restricted_nickname(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
@@ -778,6 +770,35 @@ defmodule Pleroma.User do
|> put_following_and_follower_and_featured_address()
end
+ def validate_not_restricted_nickname(changeset, field) do
+ validate_change(changeset, field, fn _, value ->
+ valid? =
+ Config.get([User, :restricted_nicknames])
+ |> Enum.all?(fn restricted_nickname ->
+ String.downcase(value) != String.downcase(restricted_nickname)
+ end)
+
+ if valid?, do: [], else: [nickname: "Invalid nickname"]
+ end)
+ end
+
+ def validate_email_not_in_blacklisted_domain(changeset, field) do
+ validate_change(changeset, field, fn _, value ->
+ valid? =
+ Config.get([User, :email_blacklist])
+ |> Enum.all?(fn blacklisted_domain ->
+ blacklisted_domain_downcase = String.downcase(blacklisted_domain)
+
+ !String.ends_with?(String.downcase(value), [
+ "@" <> blacklisted_domain_downcase,
+ "." <> blacklisted_domain_downcase
+ ])
+ end)
+
+ if valid?, do: [], else: [email: "Invalid email"]
+ end)
+ end
+
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
@@ -1459,17 +1480,30 @@ defmodule Pleroma.User do
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
notifications? = Map.get(params, :notifications, true)
- expires_in = Map.get(params, :expires_in, 0)
+ duration = Map.get(params, :duration, 0)
+
+ expires_at =
+ if duration > 0 do
+ DateTime.utc_now()
+ |> DateTime.add(duration)
+ else
+ nil
+ end
- with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
+ with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee, expires_at),
{:ok, user_notification_mute} <-
- (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
+ (notifications? &&
+ UserRelationship.create_notification_mute(
+ muter,
+ mutee,
+ expires_at
+ )) ||
{:ok, nil} do
- if expires_in > 0 do
+ if duration > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_user",
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
- schedule_in: expires_in
+ scheduled_at: expires_at
)
end
@@ -2364,6 +2398,38 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
+ def alias_users(user) do
+ user.also_known_as
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user != nil end)
+ end
+
+ def add_alias(user, new_alias_user) do
+ current_aliases = user.also_known_as || []
+ new_alias_ap_id = new_alias_user.ap_id
+
+ if new_alias_ap_id in current_aliases do
+ {:ok, user}
+ else
+ user
+ |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ end
+ end
+
+ def delete_alias(user, alias_user) do
+ current_aliases = user.also_known_as || []
+ alias_ap_id = alias_user.ap_id
+
+ if alias_ap_id in current_aliases do
+ user
+ |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ else
+ {:error, :no_such_alias}
+ end
+ end
+
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, status) do
user
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index 9cb329663..9df010605 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -32,9 +32,7 @@ defmodule Pleroma.User.Backup do
end
def create(user, admin_id \\ nil) do
- with :ok <- validate_email_enabled(),
- :ok <- validate_user_email(user),
- :ok <- validate_limit(user, admin_id),
+ with :ok <- validate_limit(user, admin_id),
{:ok, backup} <- user |> new() |> Repo.insert() do
BackupWorker.process(backup, admin_id)
end
@@ -86,20 +84,6 @@ defmodule Pleroma.User.Backup do
end
end
- defp validate_email_enabled do
- if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
- :ok
- else
- {:error, dgettext("errors", "Backups require enabled email")}
- end
- end
-
- defp validate_user_email(%User{email: nil}) do
- {:error, dgettext("errors", "Email is required")}
- end
-
- defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
-
def get_last(user_id) do
__MODULE__
|> where(user_id: ^user_id)
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
index 1432a1d83..5b3e593d3 100644
--- a/lib/pleroma/user_relationship.ex
+++ b/lib/pleroma/user_relationship.ex
@@ -18,16 +18,17 @@ defmodule Pleroma.UserRelationship do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, Pleroma.UserRelationship.Type)
+ field(:expires_at, :utc_datetime)
timestamps(updated_at: false)
end
for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do
- # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
- # `def create_notification_mute/2`, `def create_inverse_subscription/2`,
- # `def endorsement/2`
- def unquote(:"create_#{relationship_type}")(source, target),
- do: create(unquote(relationship_type), source, target)
+ # `def create_block/3`, `def create_mute/3`, `def create_reblog_mute/3`,
+ # `def create_notification_mute/3`, `def create_inverse_subscription/3`,
+ # `def endorsement/3`
+ def unquote(:"create_#{relationship_type}")(source, target, expires_at \\ nil),
+ do: create(unquote(relationship_type), source, target, expires_at)
# `def delete_block/2`, `def delete_mute/2`, `def delete_reblog_mute/2`,
# `def delete_notification_mute/2`, `def delete_inverse_subscription/2`,
@@ -37,9 +38,15 @@ defmodule Pleroma.UserRelationship do
# `def block_exists?/2`, `def mute_exists?/2`, `def reblog_mute_exists?/2`,
# `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2`,
- # `def inverse_endorsement?/2`
+ # `def inverse_endorsement_exists?/2`
def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target)
+
+ # `def get_block_expire_date/2`, `def get_mute_expire_date/2`,
+ # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`,
+ # `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2`
+ def unquote(:"get_#{relationship_type}_expire_date")(source, target),
+ do: get_expire_date(unquote(relationship_type), source, target)
end
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
@@ -48,7 +55,7 @@ defmodule Pleroma.UserRelationship do
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
- |> cast(params, [:relationship_type, :source_id, :target_id])
+ |> cast(params, [:relationship_type, :source_id, :target_id, :expires_at])
|> validate_required([:relationship_type, :source_id, :target_id])
|> unique_constraint(:relationship_type,
name: :user_relationships_source_id_relationship_type_target_id_index
@@ -62,12 +69,26 @@ defmodule Pleroma.UserRelationship do
|> Repo.exists?()
end
- def create(relationship_type, %User{} = source, %User{} = target) do
+ def get_expire_date(relationship_type, %User{} = source, %User{} = target) do
+ %UserRelationship{expires_at: expires_at} =
+ UserRelationship
+ |> where(
+ relationship_type: ^relationship_type,
+ source_id: ^source.id,
+ target_id: ^target.id
+ )
+ |> Repo.one!()
+
+ expires_at
+ end
+
+ def create(relationship_type, %User{} = source, %User{} = target, expires_at \\ nil) do
%UserRelationship{}
|> changeset(%{
relationship_type: relationship_type,
source_id: source.id,
- target_id: target.id
+ target_id: target.id,
+ expires_at: expires_at
})
|> Repo.insert(
on_conflict: {:replace_all_except, [:id]},
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index c28ea5e2f..bded254c6 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -413,7 +413,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"type" => "Move",
"actor" => origin.ap_id,
"object" => origin.ap_id,
- "target" => target.ap_id
+ "target" => target.ap_id,
+ "to" => [origin.follower_address]
}
with true <- origin.ap_id in target.also_known_as,
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
index 605ca9971..97d75ecf2 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
defp score_displayname("fedibot"), do: 1.0
defp score_displayname(_), do: 0.0
- defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
+ defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
# nickname will be a binary string except when following a relay
nick_score =
if is_binary(nickname) do
@@ -45,19 +45,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
0.0
end
- nick_score + name_score
+ # actor_type "Service" is a Bot account
+ actor_type_score =
+ if actor_type == "Service" do
+ 1.0
+ else
+ 0.0
+ end
+
+ nick_score + name_score + actor_type_score
end
defp determine_if_followbot(_), do: 0.0
+ defp bot_allowed?(%{"object" => target}, bot_actor) do
+ %User{} = user = normalize_by_ap_id(target)
+
+ User.following?(user, bot_actor)
+ end
+
@impl true
def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
%User{} = actor = normalize_by_ap_id(actor_id)
score = determine_if_followbot(actor)
- # TODO: scan biography data for keywords and score it somehow.
- if score < 0.8 do
+ if score < 0.8 || bot_allowed?(message, actor) do
{:ok, message}
else
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
index 06305235e..f66c379b5 100644
--- a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -12,6 +12,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
+ defp shortcode_matches?(shortcode, pattern) when is_binary(pattern) do
+ shortcode == pattern
+ end
+
+ defp shortcode_matches?(shortcode, pattern) do
+ String.match?(shortcode, pattern)
+ end
+
defp steal_emoji({shortcode, url}, emoji_dir_path) do
url = Pleroma.Web.MediaProxy.url(url)
@@ -72,7 +80,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
reject_emoji? =
[:mrf_steal_emoji, :rejected_shortcodes]
|> Config.get([])
- |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
+ |> Enum.find(false, fn pattern -> shortcode_matches?(shortcode, pattern) end)
!reject_emoji?
end)
@@ -122,8 +130,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
%{
key: :rejected_shortcodes,
type: {:list, :string},
- description: "Regex-list of shortcodes to reject",
- suggestions: [""]
+ description: """
+ A list of patterns or matches to reject shortcodes with.
+
+ Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
+ """,
+ suggestions: ["foo", ~r/foo/]
},
%{
key: :size_limit,
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index ca335bc8a..57c8d1dc0 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -65,6 +65,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
defp fix_replies(data), do: data
+ def fix_attachments(%{"attachment" => attachment} = data) when is_map(attachment),
+ do: Map.put(data, "attachment", [attachment])
+
+ def fix_attachments(data), do: data
+
defp fix(data) do
data
|> CommonFixes.fix_actor()
@@ -72,6 +77,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_url()
|> fix_tag()
|> fix_replies()
+ |> fix_attachments()
|> Transmogrifier.fix_emoji()
|> Transmogrifier.fix_content_map()
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
index d1c61ac82..cf6e407e0 100644
--- a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -12,14 +12,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@primary_key false
embedded_schema do
field(:type, :string)
- field(:mediaType, :string, default: "application/octet-stream")
+ field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:name, :string)
field(:blurhash, :string)
embeds_many :url, UrlObjectValidator, primary_key: false do
field(:type, :string)
field(:href, ObjectValidators.Uri)
- field(:mediaType, :string, default: "application/octet-stream")
+ field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:width, :integer)
field(:height, :integer)
end
@@ -59,13 +59,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
end
def fix_media_type(data) do
- data = Map.put_new(data, "mediaType", data["mimeType"])
-
- if is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] do
- data
- else
- Map.put(data, "mediaType", "application/octet-stream")
- end
+ Map.put_new(data, "mediaType", data["mimeType"] || "application/octet-stream")
end
defp handle_href(href, mediaType, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
index ed072b888..0858281e5 100644
--- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
@@ -49,6 +49,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp fix(data) do
data =
data
+ |> fix_emoji_qualification()
|> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing()
@@ -61,6 +62,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
end
end
+ defp fix_emoji_qualification(%{"content" => emoji} = data) do
+ new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
+
+ cond do
+ Pleroma.Emoji.is_unicode_emoji?(emoji) ->
+ data
+
+ Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
+ data |> Map.put("content", new_emoji)
+
+ true ->
+ data
+ end
+ end
+
+ defp fix_emoji_qualification(data), do: data
+
defp validate_emoji(cng) do
content = get_field(cng, :content)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index a70330f0e..d6622df86 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -203,13 +203,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
media_type =
cond do
- is_map(url) && MIME.extensions(url["mediaType"]) != [] ->
+ is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
url["mediaType"]
- is_bitstring(data["mediaType"]) && MIME.extensions(data["mediaType"]) != [] ->
+ is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
data["mediaType"]
- is_bitstring(data["mimeType"]) && MIME.extensions(data["mimeType"]) != [] ->
+ is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
data["mimeType"]
true ->
diff --git a/lib/pleroma/web/admin_api/controllers/announcement_controller.ex b/lib/pleroma/web/admin_api/controllers/announcement_controller.ex
new file mode 100644
index 000000000..6ad5fc12c
--- /dev/null
+++ b/lib/pleroma/web/admin_api/controllers/announcement_controller.ex
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.AnnouncementController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Announcement
+ alias Pleroma.Web.ControllerHelper
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:create, :delete, :change])
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action in [:index, :show])
+ action_fallback(Pleroma.Web.AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.AnnouncementOperation
+
+ defp default_limit, do: 20
+
+ def index(conn, params) do
+ limit = Map.get(params, :limit, default_limit())
+ offset = Map.get(params, :offset, 0)
+
+ announcements = Announcement.list_paginated(%{limit: limit, offset: offset})
+
+ render(conn, "index.json", announcements: announcements)
+ end
+
+ def show(conn, %{id: id} = _params) do
+ announcement = Announcement.get_by_id(id)
+
+ if is_nil(announcement) do
+ {:error, :not_found}
+ else
+ render(conn, "show.json", announcement: announcement)
+ end
+ end
+
+ def create(%{body_params: params} = conn, _params) do
+ with {:ok, announcement} <- Announcement.add(change_params(params)) do
+ render(conn, "show.json", announcement: announcement)
+ else
+ _ ->
+ {:error, 400}
+ end
+ end
+
+ def change_params(orig_params) do
+ data =
+ %{}
+ |> Pleroma.Maps.put_if_present("content", orig_params, &Map.fetch(&1, :content))
+ |> Pleroma.Maps.put_if_present("all_day", orig_params, &Map.fetch(&1, :all_day))
+
+ orig_params
+ |> Map.merge(%{data: data})
+ end
+
+ def change(%{body_params: params} = conn, %{id: id} = _params) do
+ with announcement <- Announcement.get_by_id(id),
+ {:exists, true} <- {:exists, not is_nil(announcement)},
+ {:ok, announcement} <- Announcement.update(announcement, change_params(params)) do
+ render(conn, "show.json", announcement: announcement)
+ else
+ {:exists, false} ->
+ {:error, :not_found}
+
+ _ ->
+ {:error, 400}
+ end
+ end
+
+ def delete(conn, %{id: id} = _params) do
+ case Announcement.delete_by_id(id) do
+ :ok ->
+ conn
+ |> ControllerHelper.json_response(:ok, %{})
+
+ _ ->
+ {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index 55ab6d063..a03318c0e 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -22,10 +22,58 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
+ defp translate_descriptions(descriptions, path \\ []) do
+ Enum.map(descriptions, fn desc -> translate_item(desc, path) end)
+ end
+
+ defp translate_string(str, path, type) do
+ Gettext.dpgettext(
+ Pleroma.Web.Gettext,
+ "config_descriptions",
+ Pleroma.Docs.Translator.Compiler.msgctxt_for(path, type),
+ str
+ )
+ end
+
+ defp maybe_put_translated(item, key, path) do
+ if item[key] do
+ Map.put(
+ item,
+ key,
+ translate_string(
+ item[key],
+ path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)],
+ to_string(key)
+ )
+ )
+ else
+ item
+ end
+ end
+
+ defp translate_item(item, path) do
+ item
+ |> maybe_put_translated(:label, path)
+ |> maybe_put_translated(:description, path)
+ |> translate_children(path)
+ end
+
+ defp translate_children(%{children: children} = item, path) when is_list(children) do
+ item
+ |> Map.put(
+ :children,
+ translate_descriptions(children, path ++ [Pleroma.Docs.Translator.Compiler.key_for(item)])
+ )
+ end
+
+ defp translate_children(item, _path) do
+ item
+ end
+
def descriptions(conn, _params) do
descriptions = Enum.filter(Pleroma.Docs.JSON.compiled_descriptions(), &whitelisted_config?/1)
- json(conn, descriptions)
+ json(conn, translate_descriptions(descriptions))
end
def show(conn, %{only_db: true}) do
diff --git a/lib/pleroma/web/admin_api/views/announcement_view.ex b/lib/pleroma/web/admin_api/views/announcement_view.ex
new file mode 100644
index 000000000..a35bd60cf
--- /dev/null
+++ b/lib/pleroma/web/admin_api/views/announcement_view.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.AnnouncementView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{announcements: announcements}) do
+ render_many(announcements, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{announcement: announcement}) do
+ Pleroma.Announcement.render_json(announcement, admin: true)
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index a64762285..97616f5e7 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -279,10 +279,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
"Mute notifications in addition to statuses? Defaults to `true`."
),
Operation.parameter(
+ :duration,
+ :query,
+ %Schema{type: :integer},
+ "Expire the mute in `duration` seconds. Default 0 for infinity"
+ ),
+ Operation.parameter(
:expires_in,
:query,
%Schema{type: :integer, default: 0},
- "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ "Deprecated, use `duration` instead"
)
],
responses: %{
@@ -545,10 +551,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Invite token required when the registrations aren't public"
},
birthday: %Schema{
- type: :string,
nullable: true,
description: "User's birthday",
- format: :date
+ anyOf: [
+ %Schema{
+ type: :string,
+ format: :date
+ },
+ %Schema{
+ type: :string,
+ maxLength: 0
+ }
+ ]
},
language: %Schema{
type: :string,
@@ -733,10 +747,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
},
actor_type: ActorType,
birthday: %Schema{
- type: :string,
nullable: true,
description: "User's birthday",
- format: :date
+ anyOf: [
+ %Schema{
+ type: :string,
+ format: :date
+ },
+ %Schema{
+ type: :string,
+ maxLength: 0
+ }
+ ]
},
show_birthday: %Schema{
allOf: [BooleanLike],
@@ -861,10 +883,15 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
},
+ duration: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity"
+ },
expires_in: %Schema{
type: :integer,
nullable: true,
- description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ description: "Deprecated, use `duration` instead",
default: 0
}
},
diff --git a/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex
new file mode 100644
index 000000000..58a039e72
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/admin/announcement_operation.ex
@@ -0,0 +1,165 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Announcement
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Retrieve a list of announcements",
+ operationId: "AdminAPI.AnnouncementController.index",
+ security: [%{"oAuth" => ["admin:read"]}],
+ parameters: [
+ Operation.parameter(
+ :limit,
+ :query,
+ %Schema{type: :integer, minimum: 1},
+ "the maximum number of announcements to return"
+ ),
+ Operation.parameter(
+ :offset,
+ :query,
+ %Schema{type: :integer, minimum: 0},
+ "the offset of the first announcement to return"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_announcements()),
+ 400 => Operation.response("Forbidden", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Display one announcement",
+ operationId: "AdminAPI.AnnouncementController.show",
+ security: [%{"oAuth" => ["admin:read"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Delete one announcement",
+ operationId: "AdminAPI.AnnouncementController.delete",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Create one announcement",
+ operationId: "AdminAPI.AnnouncementController.create",
+ security: [%{"oAuth" => ["admin:write"]}],
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def change_operation do
+ %Operation{
+ tags: ["Announcement managment"],
+ summary: "Change one announcement",
+ operationId: "AdminAPI.AnnouncementController.change",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ | admin_api_params()
+ ],
+ requestBody: request_body("Parameters", change_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", Announcement),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_or_change_props do
+ %{
+ content: %Schema{type: :string},
+ starts_at: %Schema{type: :string, format: "date-time", nullable: true},
+ ends_at: %Schema{type: :string, format: "date-time", nullable: true},
+ all_day: %Schema{type: :boolean}
+ }
+ end
+
+ def create_request do
+ %Schema{
+ title: "AnnouncementCreateRequest",
+ type: :object,
+ required: [:content],
+ properties: create_or_change_props()
+ }
+ end
+
+ def change_request do
+ %Schema{
+ title: "AnnouncementChangeRequest",
+ type: :object,
+ properties: create_or_change_props()
+ }
+ end
+
+ def list_of_announcements do
+ %Schema{
+ type: :array,
+ items: Announcement
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/announcement_operation.ex b/lib/pleroma/web/api_spec/operations/announcement_operation.ex
new file mode 100644
index 000000000..71be0002a
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/announcement_operation.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Announcement
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Announcement"],
+ summary: "Retrieve a list of announcements",
+ operationId: "MastodonAPI.AnnouncementController.index",
+ security: [%{"oAuth" => []}],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", list_of_announcements()),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def mark_read_operation do
+ %Operation{
+ tags: ["Announcement"],
+ summary: "Mark one announcement as read",
+ operationId: "MastodonAPI.AnnouncementController.mark_read",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "announcement id"
+ )
+ ],
+ responses: %{
+ 200 => Operation.response("Response", "application/json", %Schema{type: :object}),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def list_of_announcements do
+ %Schema{
+ type: :array,
+ items: Announcement
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 7f2336ff6..56aa129d2 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -51,6 +51,12 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
:include_types,
:query,
%Schema{type: :array, items: notification_type()},
+ "Deprecated, use `types` instead"
+ ),
+ Operation.parameter(
+ :types,
+ :query,
+ %Schema{type: :array, items: notification_type()},
"Include the notifications for activities with the given types"
),
Operation.parameter(
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
index c59e3b12a..1cc90990f 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -214,6 +214,146 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
end
+ def move_account_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Move account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.move_account",
+ requestBody: request_body("Parameters", move_account_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{status: %Schema{type: :string, example: "success"}}
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp move_account_request do
+ %Schema{
+ title: "MoveAccountRequest",
+ description: "POST body for moving the account",
+ type: :object,
+ required: [:password, :target_account],
+ properties: %{
+ password: %Schema{type: :string, description: "Current password"},
+ target_account: %Schema{
+ type: :string,
+ description: "The nickname of the target account to move to"
+ }
+ }
+ }
+ end
+
+ def list_aliases_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "List account aliases",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ operationId: "UtilController.list_aliases",
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ aliases: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["foo@example.org"]
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def add_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Add an alias to this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.add_alias",
+ requestBody: request_body("Parameters", add_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp add_alias_request do
+ %Schema{
+ title: "AddAliasRequest",
+ description: "PUT body for adding aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to add to aliases"
+ }
+ }
+ }
+ end
+
+ def delete_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Delete an alias from this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.delete_alias",
+ requestBody: request_body("Parameters", delete_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp delete_alias_request do
+ %Schema{
+ title: "DeleteAliasRequest",
+ description: "PUT body for deleting aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to delete from aliases"
+ }
+ }
+ }
+ end
+
def healthcheck_operation do
%Operation{
tags: ["Accounts"],
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
index e8a529f2e..8aeb821a8 100644
--- a/lib/pleroma/web/api_spec/schemas/account.ex
+++ b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -33,6 +33,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
header: %Schema{type: :string, format: :uri},
id: FlakeID,
locked: %Schema{type: :boolean},
+ mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
note: %Schema{type: :string, format: :html},
statuses_count: %Schema{type: :integer},
url: %Schema{type: :string, format: :uri},
diff --git a/lib/pleroma/web/api_spec/schemas/announcement.ex b/lib/pleroma/web/api_spec/schemas/announcement.ex
new file mode 100644
index 000000000..67d129ef6
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/announcement.ex
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Announcement do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Announcement",
+ description: "Response schema for an announcement",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ content: %Schema{type: :string},
+ starts_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true
+ },
+ ends_at: %Schema{
+ type: :string,
+ format: "date-time",
+ nullable: true
+ },
+ all_day: %Schema{type: :boolean},
+ published_at: %Schema{type: :string, format: "date-time"},
+ updated_at: %Schema{type: :string, format: "date-time"},
+ read: %Schema{type: :boolean},
+ mentions: %Schema{type: :array},
+ statuses: %Schema{type: :array},
+ tags: %Schema{type: :array},
+ emojis: %Schema{type: :array},
+ reactions: %Schema{type: :array},
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ raw_content: %Schema{type: :string}
+ }
+ }
+ }
+ })
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 50c12a1b1..bf931dc6b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -411,6 +411,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ params =
+ params
+ |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
+
with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
@@ -491,7 +495,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users =
user
|> User.muted_users_relation(_restrict_deactivated = true)
- |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+ |> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
@@ -499,7 +503,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users: users,
for: user,
as: :user,
- embed_relationships: embed_relationships?(params)
+ embed_relationships: embed_relationships?(params),
+ mutes: true
)
end
@@ -508,7 +513,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
users =
user
|> User.blocked_users_relation(_restrict_deactivated = true)
- |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
+ |> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
diff --git a/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex b/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex
new file mode 100644
index 000000000..080af96d5
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/announcement_controller.ex
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AnnouncementController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ json_response: 3
+ ]
+
+ alias Pleroma.Announcement
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ # Mastodon docs say this only requires a user token, no scopes needed
+ # As the op `|` requires at least one scope to be present, we use `&` here.
+ plug(
+ OAuthScopesPlug,
+ %{scopes: [], op: :&}
+ when action in [:index]
+ )
+
+ # Same as in MastodonAPI specs
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"]}
+ when action in [:mark_read]
+ )
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AnnouncementOperation
+
+ @doc "GET /api/v1/announcements"
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ render(conn, "index.json", announcements: all_visible(), user: user)
+ end
+
+ def index(conn, _params) do
+ render(conn, "index.json", announcements: all_visible(), user: nil)
+ end
+
+ defp all_visible do
+ Announcement.list_all_visible()
+ end
+
+ @doc "POST /api/v1/announcements/:id/dismiss"
+ def mark_read(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
+ with announcement when not is_nil(announcement) <- Announcement.get_by_id(id),
+ {:ok, _} <- Announcement.mark_read_by(announcement, user) do
+ json_response(conn, :ok, %{})
+ else
+ _ ->
+ {:error, :not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index 932bc6423..740cf58e7 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -55,7 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
def index(%{assigns: %{user: user}} = conn, params) do
params =
Map.new(params, fn {k, v} -> {to_string(k), v} end)
- |> Map.put_new("include_types", @default_notification_types)
+ |> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
notifications = MastodonAPI.get_notifications(user, params)
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 5e32b9611..b4d092eed 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
user
|> Notification.for_user_query(options)
- |> restrict(:include_types, options)
+ |> restrict(:types, options)
|> restrict(:exclude_types, options)
|> restrict(:account_ap_id, options)
|> Pagination.fetch_paginated(params)
@@ -80,7 +80,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string},
- include_types: {:array, :string},
+ types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean,
with_muted: :boolean,
@@ -92,7 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
changeset.changes
end
- defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
+ defp restrict(query, :types, %{types: mastodon_types = [_ | _]}) do
where(query, [n], n.type in ^mastodon_types)
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 988eedbb1..2260bf5da 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -311,6 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
+ |> maybe_put_mute_expires_at(user, opts[:for], opts)
|> maybe_show_birthday(user, opts[:for])
end
@@ -434,6 +435,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_email_address(data, _, _), do: data
+ defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do
+ Map.put(
+ data,
+ :mute_expires_at,
+ UserRelationship.get_mute_expire_date(target, user)
+ )
+ end
+
+ defp maybe_put_mute_expires_at(data, _, _, _), do: data
+
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :birthday], user.birthday)
diff --git a/lib/pleroma/web/mastodon_api/views/announcement_view.ex b/lib/pleroma/web/mastodon_api/views/announcement_view.ex
new file mode 100644
index 000000000..93fdfb1f1
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/announcement_view.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AnnouncementView do
+ use Pleroma.Web, :view
+
+ def render("index.json", %{announcements: announcements, user: user}) do
+ render_many(announcements, __MODULE__, "show.json", user: user)
+ end
+
+ def render("show.json", %{announcement: announcement, user: user}) do
+ Pleroma.Announcement.render_json(announcement, for: user)
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index ee52475d5..62931bd41 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
uri: Pleroma.Web.Endpoint.url(),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
+ short_description: Keyword.get(instance, :short_description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index ceb6c3cfd..e953116c5 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -229,6 +229,12 @@ defmodule Pleroma.Web.Router do
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
+
+ get("/announcements", AnnouncementController, :index)
+ post("/announcements", AnnouncementController, :create)
+ get("/announcements/:id", AnnouncementController, :show)
+ patch("/announcements/:id", AnnouncementController, :change)
+ delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if enabled by config)
@@ -343,6 +349,11 @@ defmodule Pleroma.Web.Router do
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
+ post("/move_account", UtilController, :move_account)
+
+ put("/aliases", UtilController, :add_alias)
+ get("/aliases", UtilController, :list_aliases)
+ delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
@@ -575,6 +586,9 @@ defmodule Pleroma.Web.Router do
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
+
+ get("/announcements", AnnouncementController, :index)
+ post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
@@ -669,11 +683,6 @@ defmodule Pleroma.Web.Router do
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
- # Notice compatibility routes for other frontends
- get("/@:nickname/:id", OStatus.OStatusController, :notice)
- get("/@:nickname/posts/:id", OStatus.OStatusController, :notice)
- get("/:nickname/status/:id", OStatus.OStatusController, :notice)
-
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
index b20a3689c..97c41c6f9 100644
--- a/lib/pleroma/web/static_fe/static_fe_controller.ex
+++ b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -167,15 +167,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id)
- defp assign_id(%{path_info: ["@" <> _nickname, notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
- defp assign_id(%{path_info: ["@" <> _nickname, "posts", notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
- defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts),
- do: assign(conn, :notice_id, notice_id)
-
defp assign_id(%{path_info: ["users", user_id]} = conn, _opts),
do: assign(conn, :username_or_id, user_id)
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 53465b124..5731c78a8 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
@@ -26,7 +27,18 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
:change_password,
:delete_account,
:update_notificaton_settings,
- :disable_account
+ :disable_account,
+ :move_account,
+ :add_alias,
+ :delete_alias
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]}
+ when action in [
+ :list_aliases
]
)
@@ -158,6 +170,91 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
+ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
+ {:ok, user} ->
+ with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
+ {:ok, _user} <- ActivityPub.move(user, target_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account not found."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+
+ {:error, msg} ->
+ json(conn, %{error: msg})
+ end
+ end
+
+ def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.add_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account does not exist."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.delete_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:error, :no_such_alias} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Account has no such alias."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
+ alias_nicks =
+ user
+ |> User.alias_users()
+ |> Enum.map(&User.full_nickname/1)
+
+ json(conn, %{aliases: alias_nicks})
+ end
+
+ defp find_user_by_nickname(nickname) do
+ user = User.get_cached_by_nickname(nickname)
+
+ if user == nil do
+ {:not_found, nil}
+ else
+ {:ok, user}
+ end
+ end
+
+ defp find_or_fetch_user_by_nickname(nickname) do
+ user = User.get_by_nickname(nickname)
+
+ if user != nil and user.local do
+ {:ok, user}
+ else
+ with {:ok, user} <- User.fetch_by_nickname(nickname) do
+ {:ok, user}
+ else
+ _ ->
+ {:not_found, nil}
+ end
+ end
+ end
+
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
index bd33d4c0a..8902261b0 100644
--- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
+++ b/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
@@ -7,5 +7,9 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext
- defdelegate avatar_url(user), to: Pleroma.User
+ def avatar_url(user) do
+ user
+ |> Pleroma.User.avatar_url()
+ |> Pleroma.Web.MediaProxy.url()
+ end
end
diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex
index 3caef85b7..7657fa9ce 100644
--- a/lib/pleroma/workers/backup_worker.ex
+++ b/lib/pleroma/workers/backup_worker.ex
@@ -37,10 +37,7 @@ defmodule Pleroma.Workers.BackupWorker do
backup_id |> Backup.get() |> Backup.process(),
{:ok, _job} <- schedule_deletion(backup),
:ok <- Backup.remove_outdated(backup),
- {:ok, _} <-
- backup
- |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
- |> Pleroma.Emails.Mailer.deliver() do
+ :ok <- maybe_deliver_email(backup, admin_user_id) do
{:ok, backup}
end
end
@@ -51,4 +48,23 @@ defmodule Pleroma.Workers.BackupWorker do
nil -> :ok
end
end
+
+ defp has_email?(user) do
+ not is_nil(user.email) and user.email != ""
+ end
+
+ defp maybe_deliver_email(backup, admin_user_id) do
+ has_mailer = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
+ backup = backup |> Pleroma.Repo.preload(:user)
+
+ if has_email?(backup.user) and has_mailer do
+ backup
+ |> Pleroma.Emails.UserEmail.backup_is_ready_email(admin_user_id)
+ |> Pleroma.Emails.Mailer.deliver()
+
+ :ok
+ else
+ :ok
+ end
+ end
end