Merge branch 'develop' into 'remove-avatar-header'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
Sachin Joshi 2019-06-23 03:25:50 +00:00
commit a0c65bbd6c
237 changed files with 8534 additions and 2226 deletions

View file

@ -343,4 +343,6 @@ defmodule Pleroma.Activity do
)
)
end
defdelegate search(user, query), to: Pleroma.Activity.Search
end

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Search do
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
import Ecto.Query
def search(user, search_query) do
index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public()
|> query_with(index_type, search_query)
|> maybe_restrict_local(user)
|> Repo.all()
|> maybe_fetch(user, search_query)
end
defp restrict_public(q) do
from([a, o] in q,
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
limit: 40
)
end
defp query_with(q, :gin, search_query) do
from([a, o] in q,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^search_query
)
)
end
defp query_with(q, :rum, search_query) do
from([a, o] in q,
where:
fragment(
"? @@ plainto_tsquery('english', ?)",
o.fts_content,
^search_query
),
order_by: [fragment("? <=> now()::date", o.inserted_at)]
)
end
defp maybe_restrict_local(q, user) do
limit = Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
case {limit, user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp restrict_local(q), do: where(q, local: true)
defp maybe_fetch(activities, user, search_query) do
with true <- Regex.match?(~r/https?:/, search_query),
{:ok, object} <- Fetcher.fetch_object_from_id(search_query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
activities ++ [activity]
else
_ -> activities
end
end
end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Application do
use Application
import Supervisor.Spec
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@ -31,96 +30,128 @@ defmodule Pleroma.Application do
children =
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
worker(Pleroma.Captcha, []),
worker(
Cachex,
[
:used_captcha_cache,
[
ttl_interval: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
]
],
id: :cachex_used_captcha_cache
),
worker(
Cachex,
[
:user_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_user
),
worker(
Cachex,
[
:object_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_object
),
worker(
Cachex,
[
:rich_media_cache,
[
default_ttl: :timer.minutes(120),
limit: 5000
]
],
id: :cachex_rich_media
),
worker(
Cachex,
[
:scrubber_cache,
[
limit: 2500
]
],
id: :cachex_scrubber
),
worker(
Cachex,
[
:idempotency_cache,
[
expiration:
expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500
]
],
id: :cachex_idem
),
worker(Pleroma.FlakeId, []),
worker(Pleroma.ScheduledActivityWorker, [])
%{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor},
%{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}},
%{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}},
%{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}},
%{
id: :cachex_used_captcha_cache,
start:
{Cachex, :start_link,
[
:used_captcha_cache,
[
ttl_interval:
:timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
]
]}
},
%{
id: :cachex_user,
start:
{Cachex, :start_link,
[
:user_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
]}
},
%{
id: :cachex_object,
start:
{Cachex, :start_link,
[
:object_cache,
[
default_ttl: 25_000,
ttl_interval: 1000,
limit: 2500
]
]}
},
%{
id: :cachex_rich_media,
start:
{Cachex, :start_link,
[
:rich_media_cache,
[
default_ttl: :timer.minutes(120),
limit: 5000
]
]}
},
%{
id: :cachex_scrubber,
start:
{Cachex, :start_link,
[
:scrubber_cache,
[
limit: 2500
]
]}
},
%{
id: :cachex_idem,
start:
{Cachex, :start_link,
[
:idempotency_cache,
[
expiration:
expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500
]
]}
},
%{id: Pleroma.FlakeId, start: {Pleroma.FlakeId, :start_link, []}},
%{
id: Pleroma.ScheduledActivityWorker,
start: {Pleroma.ScheduledActivityWorker, :start_link, []}
}
] ++
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Web.OAuth.Token.CleanWorker, []),
worker(Pleroma.Stats, []),
worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init),
worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
%{
id: Pleroma.Web.Federator.RetryQueue,
start: {Pleroma.Web.Federator.RetryQueue, :start_link, []}
},
%{
id: Pleroma.Web.OAuth.Token.CleanWorker,
start: {Pleroma.Web.OAuth.Token.CleanWorker, :start_link, []}
},
%{
id: Pleroma.Stats,
start: {Pleroma.Stats, :start_link, []}
},
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
},
%{
id: :federator_init,
start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
restart: :temporary
}
] ++
streamer_child() ++
chat_child() ++
[
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
worker(Pleroma.Gopher.Server, [])
%{
id: Pleroma.Web.Endpoint,
start: {Pleroma.Web.Endpoint, :start_link, []},
type: :supervisor
},
%{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
@ -144,7 +175,6 @@ defmodule Pleroma.Application do
Pleroma.Repo.Instrumenter.setup()
end
Prometheus.Registry.register_collector(:prometheus_process_collector)
Pleroma.Web.Endpoint.MetricsExporter.setup()
Pleroma.Web.Endpoint.PipelineInstrumenter.setup()
Pleroma.Web.Endpoint.Instrumenter.setup()
@ -157,24 +187,29 @@ defmodule Pleroma.Application do
else
[]
end ++
if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do
if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
[:upload]
else
[]
end
end
if Mix.env() == :test do
if Pleroma.Config.get(:env) == :test do
defp streamer_child, do: []
defp chat_child, do: []
else
defp streamer_child do
[worker(Pleroma.Web.Streamer, [])]
[%{id: Pleroma.Web.Streamer, start: {Pleroma.Web.Streamer, :start_link, []}}]
end
defp chat_child do
if Pleroma.Config.get([:chat, :enabled]) do
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
[
%{
id: Pleroma.Web.ChatChannel.ChatChannelState,
start: {Pleroma.Web.ChatChannel.ChatChannelState, :start_link, []}
}
]
else
[]
end

View file

@ -0,0 +1,42 @@
defmodule Pleroma.Config.TransferTask do
use Task
alias Pleroma.Web.AdminAPI.Config
def start_link do
load_and_update_env()
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
:ignore
end
def load_and_update_env do
if Pleroma.Config.get([:instance, :dynamic_configuration]) and
Ecto.Adapters.SQL.table_exists?(Pleroma.Repo, "config") do
Pleroma.Repo.all(Config)
|> Enum.each(&update_env(&1))
end
end
defp update_env(setting) do
try do
key =
if String.starts_with?(setting.key, "Pleroma.") do
"Elixir." <> setting.key
else
setting.key
end
Application.put_env(
:pleroma,
String.to_existing_atom(key),
Config.from_binary(setting.value)
)
rescue
e ->
require Logger
Logger.warn(
"updating env causes error, key: #{inspect(setting.key)}, error: #{inspect(e)}"
)
end
end
end

View file

@ -49,7 +49,7 @@ defmodule Pleroma.Conversation do
with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
"Create" <- activity.data["type"],
object <- Pleroma.Object.normalize(activity),
"Note" <- object.data["type"],
true <- object.data["type"] in ["Note", "Question"],
ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do
{:ok, conversation} = create_for_ap_id(ap_id)

View file

@ -59,10 +59,10 @@ defmodule Pleroma.Conversation.Participation do
def for_user(user, params \\ %{}) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
order_by: [desc: p.updated_at]
order_by: [desc: p.updated_at],
preload: [conversation: [:users]]
)
|> Pleroma.Pagination.fetch_paginated(params)
|> Repo.preload(conversation: [:users])
end
def for_user_with_last_activity_id(user, params \\ %{}) do
@ -79,5 +79,6 @@ defmodule Pleroma.Conversation.Participation do
| last_activity_id: activity_id
}
end)
|> Enum.filter(& &1.last_activity_id)
end
end

View file

@ -22,7 +22,6 @@ defmodule Pleroma.Emoji do
@ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@groups Application.get_env(:pleroma, :emoji)[:groups]
@doc false
def start_link do
@ -87,6 +86,8 @@ defmodule Pleroma.Emoji do
"emoji"
)
emoji_groups = Pleroma.Config.get([:emoji, :groups])
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
@ -97,14 +98,28 @@ defmodule Pleroma.Emoji do
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
{:ok, packs} ->
{:ok, results} ->
grouped =
Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
packs = grouped[true] || []
files = grouped[false] || []
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end
emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
)
true = :ets.insert(@ets, emojis)
@ -112,12 +127,12 @@ defmodule Pleroma.Emoji do
# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])
emojis =
(load_from_file("config/emoji.txt") ++
load_from_file("config/custom_emoji.txt") ++
load_from_globs(shortcode_globs))
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)
true = :ets.insert(@ets, emojis)
@ -125,13 +140,13 @@ defmodule Pleroma.Emoji do
:ok
end
defp load_pack(pack_dir) do
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
load_from_file(emoji_txt)
load_from_file(emoji_txt, emoji_groups)
else
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
@ -141,7 +156,7 @@ defmodule Pleroma.Emoji do
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(@groups, filename))]}
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end
@ -170,21 +185,21 @@ defmodule Pleroma.Emoji do
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end
defp load_from_file(file) do
defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file))
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end
defp load_from_file_stream(stream) do
defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(@groups, file))]}
{name, file, [to_string(match_extra(emoji_groups, file))]}
[name, file | tags] ->
{name, file, tags}
@ -196,7 +211,7 @@ defmodule Pleroma.Emoji do
|> Enum.to_list()
end
defp load_from_globs(globs) do
defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")
paths =
@ -207,7 +222,7 @@ defmodule Pleroma.Emoji do
|> Enum.concat()
Enum.map(paths, fn path ->
tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Formatter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>@.+?\s+)+)(?<rest>.*)/s
@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Helpers.UriHelper do
def append_uri_params(uri, appended_params) do
uri = URI.parse(uri)
appended_params = for {k, v} <- appended_params, into: %{}, do: {to_string(k), v}
existing_params = URI.query_decoder(uri.query || "") |> Enum.into(%{})
updated_params_keys = Enum.uniq(Map.keys(existing_params) ++ Map.keys(appended_params))
updated_params =
for k <- updated_params_keys, do: {k, appended_params[k] || existing_params[k]}
uri
|> Map.put(:query, URI.encode_query(updated_params))
|> URI.to_string()
end
def append_param_if_present(%{} = params, param_name, param_value) do
if param_value do
Map.put(params, param_name, param_value)
else
params
end
end
end

View file

@ -89,7 +89,7 @@ defmodule Pleroma.HTML do
Cachex.fetch!(:scrubber_cache, key, fn _key ->
result =
content
|> Floki.filter_out("a.mention")
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href")
|> Enum.at(0)
@ -104,7 +104,6 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
paragraphs, breaks and links are allowed through the filter.
"""
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
require HtmlSanitizeEx.Scrubber.Meta
@ -142,9 +141,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
if Pleroma.Config.get([:markup, :allow_inline_images]) do
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
@ -168,7 +165,6 @@ defmodule Pleroma.HTML.Scrubber.Default do
# credo:disable-for-previous-line
# No idea how to fix this one…
@markup Application.get_env(:pleroma, :markup)
@valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], [])
Meta.remove_cdata_sections_before_scrub()
@ -213,7 +209,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
@allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images])
if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy.
@ -228,9 +224,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
])
end
@allow_tables Keyword.get(@markup, :allow_tables)
if @allow_tables do
if Pleroma.Config.get([:markup, :allow_tables]) do
Meta.allow_tag_with_these_attributes("table", [])
Meta.allow_tag_with_these_attributes("tbody", [])
Meta.allow_tag_with_these_attributes("td", [])
@ -239,9 +233,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("tr", [])
end
@allow_headings Keyword.get(@markup, :allow_headings)
if @allow_headings do
if Pleroma.Config.get([:markup, :allow_headings]) do
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", [])
@ -249,9 +241,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("h5", [])
end
@allow_fonts Keyword.get(@markup, :allow_fonts)
if @allow_fonts do
if Pleroma.Config.get([:markup, :allow_fonts]) do
Meta.allow_tag_with_these_attributes("font", ["face"])
end

View file

@ -32,9 +32,11 @@ defmodule Pleroma.HTTP.Connection do
defp hackney_options(opts) do
options = Keyword.get(opts, :adapter, [])
adapter_options = Pleroma.Config.get([:http, :adapter], [])
proxy_url = Pleroma.Config.get([:http, :proxy_url], nil)
@hackney_options
|> Keyword.merge(adapter_options)
|> Keyword.merge(options)
|> Keyword.merge(proxy: proxy_url)
end
end

View file

@ -65,12 +65,9 @@ defmodule Pleroma.HTTP do
end
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil)
case proxy do
case Pleroma.Config.get([:http, :proxy_url]) do
nil -> options
_ -> options ++ [proxy: proxy]
proxy -> options ++ [proxy: proxy]
end
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Instances do
def reachability_datetime_threshold do
federation_reachability_timeout_days =
Pleroma.Config.get(:instance)[:federation_reachability_timeout_days] || 0
Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0)
if federation_reachability_timeout_days > 0 do
NaiveDateTime.add(

View file

@ -13,6 +13,8 @@ defmodule Pleroma.Notification do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
@ -125,10 +127,21 @@ defmodule Pleroma.Notification do
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
@ -140,8 +153,9 @@ defmodule Pleroma.Notification do
unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
Pleroma.Web.Push.send(notification)
Streamer.stream("user", notification)
Streamer.stream("user:notification", notification)
Push.send(notification)
notification
end
end
@ -166,7 +180,16 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(_, _local_only), do: []
def skip?(activity, user) do
[:self, :blocked, :local, :muted, :followers, :follows, :recently_followed]
[
:self,
:blocked,
:muted,
:followers,
:follows,
:non_followers,
:non_follows,
:recently_followed
]
|> Enum.any?(&skip?(&1, activity, user))
end
@ -179,12 +202,6 @@ defmodule Pleroma.Notification do
User.blocks?(user, %{ap_id: actor})
end
def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}),
do: true
def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}),
do: true
def skip?(:muted, activity, user) do
actor = activity.data["actor"]
@ -201,12 +218,32 @@ defmodule Pleroma.Notification do
User.following?(follower, user)
end
def skip?(
:non_followers,
activity,
%{info: %{notification_settings: %{"non_followers" => false}}} = user
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user)
end
def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed)
end
def skip?(
:non_follows,
activity,
%{info: %{notification_settings: %{"non_follows" => false}}} = user
) do
actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed)
end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
actor = activity.data["actor"]

View file

@ -35,6 +35,9 @@ defmodule Pleroma.Object do
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
@ -195,4 +198,34 @@ defmodule Pleroma.Object do
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
options =
(object.data["anyOf"] || object.data["oneOf"] || [])
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
end

View file

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object.Containment do
@moduledoc """
This module contains some useful functions for containing objects to specific

View file

@ -1,4 +1,5 @@
defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Web.ActivityPub.Transmogrifier
@ -6,8 +7,6 @@ defmodule Pleroma.Object.Fetcher do
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
defp reinject_object(data) do
Logger.debug("Reinjecting object #{data["id"]}")
@ -78,7 +77,7 @@ defmodule Pleroma.Object.Fetcher do
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
HTTP.get(
id,
[{:Accept, "application/activity+json"}]
),
@ -86,6 +85,9 @@ defmodule Pleroma.Object.Fetcher do
:ok <- Containment.contain_origin_from_id(id, data) do
{:ok, data}
else
{:ok, %{status: code}} when code in [404, 410] ->
{:error, "Object has been deleted"}
e ->
{:error, e}
end

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.FederatingPlug do
end
def call(conn, _opts) do
if Keyword.get(Application.get_env(:pleroma, :instance), :federating) do
if Pleroma.Config.get([:instance, :federating]) do
conn
else
conn

View file

@ -56,14 +56,14 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
connect_src =
if Mix.env() == :dev do
if Pleroma.Config.get(:env) == :dev do
connect_src <> " http://localhost:3035/"
else
connect_src
end
script_src =
if Mix.env() == :dev do
if Pleroma.Config.get(:env) == :dev do
"script-src 'self' 'unsafe-eval'"
else
"script-src 'self'"

View file

@ -1,36 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimitPlug do
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
def init(opts), do: opts
def call(conn, opts) do
enabled? = Pleroma.Config.get([:app_account_creation, :enabled])
case check_rate(conn, Map.put(opts, :enabled, enabled?)) do
{:ok, _count} -> conn
{:error, _count} -> render_error(conn)
%Plug.Conn{} = conn -> conn
end
end
defp check_rate(conn, %{enabled: true} = opts) do
max_requests = opts[:max_requests]
bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests)
end
defp check_rate(conn, _), do: conn
defp render_error(conn) do
conn
|> put_status(:forbidden)
|> json(%{error: "Rate limit exceeded."})
|> halt()
end
end

View file

@ -0,0 +1,94 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.RateLimiter do
@moduledoc """
## Configuration
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
To disable a limiter set its value to `nil`.
### Example
config :pleroma, :rate_limit,
one: {1000, 10},
two: [{10_000, 10}, {10_000, 50}],
foobar: nil
Here we have three limiters:
* `one` which is not over 10req/1s
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
* `foobar` which is disabled
## Usage
Inside a controller:
plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
or inside a router pipiline:
pipeline :api do
...
plug(Pleroma.Plugs.RateLimiter, :one)
...
end
"""
import Phoenix.Controller, only: [json: 2]
import Plug.Conn
alias Pleroma.User
def init(limiter_name) do
case Pleroma.Config.get([:rate_limit, limiter_name]) do
nil -> nil
config -> {limiter_name, config}
end
end
# do not limit if there is no limiter configuration
def call(conn, nil), do: conn
def call(conn, opts) do
case check_rate(conn, opts) do
{:ok, _count} -> conn
{:error, _count} -> render_error(conn)
end
end
defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do
ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit)
end
defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do
ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit)
end
defp check_rate(conn, {limiter_name, {scale, limit}}) do
check_rate(conn, {limiter_name, [{scale, limit}]})
end
def ip(%{remote_ip: remote_ip}) do
remote_ip
|> Tuple.to_list()
|> Enum.join(".")
end
defp render_error(conn) do
conn
|> put_status(:too_many_requests)
|> json(%{error: "Throttled"})
|> halt()
end
end

View file

@ -36,7 +36,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
conn
end
config = Pleroma.Config.get([Pleroma.Upload])
config = Pleroma.Config.get(Pleroma.Upload)
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),

View file

@ -0,0 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo
def run(args) do
[task | args] = String.split(args)
case task do
"migrate" -> migrate(args)
"create" -> create()
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
defp mix_task(task, args) do
Application.load(:pleroma)
{:ok, modules} = :application.get_key(:pleroma, :modules)
module =
Enum.find(modules, fn module ->
module = Module.split(module)
match?(["Mix", "Tasks", "Pleroma" | _], module) and
String.downcase(List.last(module)) == task
end)
if module do
module.run(args)
else
IO.puts("The task #{task} does not exist")
end
end
def migrate(args) do
Mix.Tasks.Pleroma.Ecto.Migrate.run(args)
end
def rollback(args) do
Mix.Tasks.Pleroma.Ecto.Rollback.run(args)
end
def create do
Application.load(:pleroma)
case @repo.__adapter__.storage_up(@repo.config) do
:ok ->
IO.puts("The database for #{inspect(@repo)} has been created")
{:error, :already_up} ->
IO.puts("The database for #{inspect(@repo)} has already been created")
{:error, term} when is_binary(term) ->
IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
{:error, term} ->
IO.puts(
:stderr,
"The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
)
end
end
end

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do
alias Pleroma.HTTP
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control)
@ -59,9 +61,6 @@ defmodule Pleroma.ReverseProxy do
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Application.get_env(:pleroma, :hackney, :hackney)
@httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
@default_hackney_options []
@inline_content_types [
@ -97,7 +96,7 @@ defmodule Pleroma.ReverseProxy do
hackney_opts =
@default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, []))
|> @httpoison.process_request_options()
|> HTTP.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts)
@ -147,7 +146,7 @@ defmodule Pleroma.ReverseProxy do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
case @hackney.request(method, url, headers, "", hackney_opts) do
case hackney().request(method, url, headers, "", hackney_opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
@ -197,7 +196,7 @@ defmodule Pleroma.ReverseProxy do
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
{:ok, data} <- @hackney.stream_body(client),
{:ok, data} <- hackney().stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
@ -378,4 +377,6 @@ defmodule Pleroma.ReverseProxy do
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
defp hackney, do: Pleroma.Config.get(:hackney, :hackney)
end

View file

@ -4,11 +4,10 @@
defmodule Pleroma.Uploaders.MDII do
alias Pleroma.Config
alias Pleroma.HTTP
@behaviour Pleroma.Uploaders.Uploader
@httpoison Application.get_env(:pleroma, :httpoison)
# MDII-hosted images are never passed through the MediaPlug; only local media.
# Delegate to Pleroma.Uploaders.Local
def get_file(file) do
@ -25,7 +24,7 @@ defmodule Pleroma.Uploaders.MDII do
query = "#{cgi}?#{extension}"
with {:ok, %{status: 200, body: body}} <-
@httpoison.post(query, file_data, [], adapter: [pool: :default]) do
HTTP.post(query, file_data, [], adapter: [pool: :default]) do
remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, {:url, public_url}}

View file

@ -324,14 +324,6 @@ defmodule Pleroma.User do
end
end
def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
if not following?(follower, followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
@ -366,14 +358,12 @@ defmodule Pleroma.User do
end
def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
ap_followers = followed.follower_address
cond do
following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
info.deactivated ->
{:error, "Could not follow user: You are deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
@ -737,122 +727,6 @@ defmodule Pleroma.User do
|> Repo.all()
end
def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve, do: get_or_fetch(query)
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
Repo.all(search_query(query, for_user))
end)
results
end
def search_query(query, for_user) do
fts_subquery = fts_search_subquery(query)
trigram_subquery = trigram_search_subquery(query)
union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
order_by: [desc: s.search_rank],
limit: 20
)
end
defp boost_search_rank_query(query, nil), do: query
defp boost_search_rank_query(query, for_user) do
friends_ids = get_friends_ids(for_user)
followers_ids = get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
CASE WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.2
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.search_rank
)
}
)
end
defp fts_search_subquery(term, query \\ User) do
processed_query =
term
|> String.replace(~r/\W+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
ts_rank_cd(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
to_tsquery('simple', ?),
32
)
""",
u.nickname,
u.name,
^processed_query
)
},
where:
fragment(
"""
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^processed_query
)
)
|> restrict_deactivated()
end
defp trigram_search_subquery(term) do
from(
u in User,
select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
|> restrict_deactivated()
end
def mute(muter, %User{ap_id: ap_id}) do
info_cng =
muter.info
@ -1162,9 +1036,7 @@ defmodule Pleroma.User do
Pleroma.HTML.Scrubber.TwitterText
end
@default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
def html_filter_policy(_), do: @default_scrubbers
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
@ -1443,4 +1315,14 @@ defmodule Pleroma.User do
update_and_set_cache(cng)
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defdelegate search(query, opts \\ []), to: User.Search
end

View file

@ -42,14 +42,21 @@ defmodule Pleroma.User.Info do
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil)
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:pleroma_settings_store, :map, default: %{})
field(:notification_settings, :map,
default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
default: %{
"followers" => true,
"follows" => true,
"non_follows" => true,
"non_followers" => true
}
)
field(:skip_thread_containment, :boolean, default: false)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
@ -68,10 +75,15 @@ defmodule Pleroma.User.Info do
end
def update_notification_settings(info, settings) do
settings =
settings
|> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
|> Map.new()
notification_settings =
info.notification_settings
|> Map.merge(settings)
|> Map.take(["remote", "local", "followers", "follows"])
|> Map.take(["followers", "follows", "non_follows", "non_followers"])
params = %{notification_settings: notification_settings}
@ -209,7 +221,9 @@ defmodule Pleroma.User.Info do
:hide_followers,
:hide_favorites,
:background,
:show_role
:show_role,
:skip_thread_containment,
:pleroma_settings_store
])
end
@ -241,14 +255,6 @@ defmodule Pleroma.User.Info do
|> validate_required([:settings])
end
def mastodon_flavour_update(info, flavour) do
params = %{flavour: flavour}
info
|> cast(params, [:flavour])
|> validate_required([:flavour])
end
def mascot_update(info, url) do
params = %{mascot: url}

191
lib/pleroma/user/search.ex Normal file
View file

@ -0,0 +1,191 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Search do
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
@similarity_threshold 0.25
@limit 20
def search(query_string, opts \\ []) do
resolve = Keyword.get(opts, :resolve, false)
following = Keyword.get(opts, :following, false)
result_limit = Keyword.get(opts, :limit, @limit)
offset = Keyword.get(opts, :offset, 0)
for_user = Keyword.get(opts, :for_user)
# Strip the beginning @ off if there is a query
query_string = String.trim_leading(query_string, "@")
maybe_resolve(resolve, for_user, query_string)
{:ok, results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(
Repo,
"select set_limit(#{@similarity_threshold})",
[]
)
query_string
|> search_query(for_user, following)
|> paginate(result_limit, offset)
|> Repo.all()
end)
results
end
defp search_query(query_string, for_user, following) do
for_user
|> base_query(following)
|> search_subqueries(query_string)
|> union_subqueries
|> distinct_query()
|> boost_search_rank_query(for_user)
|> subquery()
|> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user)
end
defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user)
defp paginate(query, limit, offset) do
from(q in query, limit: ^limit, offset: ^offset)
end
defp union_subqueries({fts_subquery, trigram_subquery}) do
from(s in trigram_subquery, union_all: ^fts_subquery)
end
defp search_subqueries(base_query, query_string) do
{
fts_search_subquery(base_query, query_string),
trigram_search_subquery(base_query, query_string)
}
end
defp distinct_query(q) do
from(s in subquery(q), order_by: s.search_type, distinct: s.id)
end
defp maybe_resolve(true, user, query) do
case {limit(), user} do
{:all, _} -> :noop
{:unauthenticated, %User{}} -> User.get_or_fetch(query)
{:unauthenticated, _} -> :noop
{false, _} -> User.get_or_fetch(query)
end
end
defp maybe_resolve(_, _, _), do: :noop
defp maybe_restrict_local(q, user) do
case {limit(), user} do
{:all, _} -> restrict_local(q)
{:unauthenticated, %User{}} -> q
{:unauthenticated, _} -> restrict_local(q)
{false, _} -> q
end
end
defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
defp restrict_local(q), do: where(q, [u], u.local == true)
defp boost_search_rank_query(query, nil), do: query
defp boost_search_rank_query(query, for_user) do
friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_ids(for_user)
from(u in subquery(query),
select_merge: %{
search_rank:
fragment(
"""
CASE WHEN (?) THEN 0.5 + (?) * 1.3
WHEN (?) THEN 0.5 + (?) * 1.2
WHEN (?) THEN (?) * 1.1
ELSE (?) END
""",
u.id in ^friends_ids and u.id in ^followers_ids,
u.search_rank,
u.id in ^friends_ids,
u.search_rank,
u.id in ^followers_ids,
u.search_rank,
u.search_rank
)
}
)
end
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp fts_search_subquery(query, term) do
processed_query =
term
|> String.replace(~r/\W+/, " ")
|> String.trim()
|> String.split()
|> Enum.map(&(&1 <> ":*"))
|> Enum.join(" | ")
from(
u in query,
select_merge: %{
search_type: ^0,
search_rank:
fragment(
"""
ts_rank_cd(
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
to_tsquery('simple', ?),
32
)
""",
u.nickname,
u.name,
^processed_query
)
},
where:
fragment(
"""
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
""",
u.nickname,
u.name,
^processed_query
)
)
|> User.restrict_deactivated()
end
@spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
defp trigram_search_subquery(query, term) do
from(
u in query,
select_merge: %{
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
search_type: fragment("?", 1),
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
^term,
u.nickname,
u.name
)
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
|> User.restrict_deactivated()
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation
alias Pleroma.Notification
alias Pleroma.Object
@ -73,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit])
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
@ -108,6 +109,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def decrease_replies_count_if_reply(_object), do: :noop
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create"
}) do
Object.increase_vote_count(reply_ap_id, name)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
def insert(map, local \\ true, fake \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
@ -183,40 +193,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
object = Object.normalize(activity)
# Do not stream out poll replies
unless object.data["type"] == "Answer" do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.data["type"] in ["Create"] do
object = Object.normalize(activity)
if activity.data["type"] in ["Create"] do
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
@ -235,6 +247,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
@ -399,16 +412,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
ap_config = Application.get_env(:pleroma, :activitypub)
unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked)
outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
with true <- unfollow_blocked do
if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
@ -480,6 +489,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
@ -492,6 +502,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
^context
)
)
|> exclude_poll_votes(opts)
|> order_by([activity], desc: activity.id)
end
@ -499,7 +510,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Activity.with_preloaded_object()
|> Repo.all()
end
@ -507,7 +517,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
@ -548,14 +558,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
query =
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
query
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
@ -565,17 +572,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do
query =
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
query
defp restrict_thread_visibility(
query,
%{"user" => %User{info: %{skip_thread_containment: true}}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end
defp restrict_thread_visibility(query, _), do: query
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
@ -653,20 +667,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_tag(query, _), do: query
defp restrict_to_cc(query, recipients_to, recipients_cc) do
from(
activity in query,
where:
fragment(
"(?->'to' \\?| ?) or (?->'cc' \\?| ?)",
activity.data,
^recipients_to,
activity.data,
^recipients_cc
)
)
end
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
@ -820,6 +820,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
@ -856,6 +868,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
base_query
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
@ -875,12 +891,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
@ -889,9 +906,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
"https://www.w3.org/ns/activitystreams#Public" in activity.recipients)
)
end
def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do
fetch_activities_query([], opts)
|> restrict_to_cc(recipients_to, recipients_cc)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn

View file

@ -5,8 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF do
@callback filter(Map.t()) :: {:ok | :reject, Map.t()}
def filter(object) do
get_policies()
def filter(policies, %{} = object) do
policies
|> Enum.reduce({:ok, object}, fn
policy, {:ok, object} ->
policy.filter(object)
@ -16,10 +16,10 @@ defmodule Pleroma.Web.ActivityPub.MRF do
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
def get_policies do
Application.get_env(:pleroma, :instance, [])
|> Keyword.get(:rewrite_policy, [])
|> get_policies()
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
end
defp get_policies(policy) when is_atom(policy), do: [policy]

View file

@ -0,0 +1,48 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
alias Pleroma.User
require Logger
# has the user successfully posted before?
defp old_user?(%User{} = u) do
u.info.note_count > 0 || u.info.follower_count > 0
end
# does the post contain links?
defp contains_links?(%{"content" => content} = _object) do
content
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl")
|> Floki.attribute("a", "href")
|> length() > 0
end
defp contains_links?(_), do: false
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, message}
else
{:contains_links, false} ->
{:ok, message}
{:old_user, false} ->
{:reject, nil}
{:error, _} ->
{:reject, nil}
e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
{:reject, nil}
end
end
# in all other cases, pass through
def filter(message), do: {:ok, message}
end

View file

@ -74,8 +74,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
actor_host
),
user <- User.get_cached_by_ap_id(object["actor"]),
true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"],
true <- user.follower_address in object["cc"] do
true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do
to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address]

View file

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
@behaviour MRF
defp lookup_subchain(actor) do
with matches <- Config.get([:mrf_subchain, :match_actor]),
{match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do
{:ok, match, subchain}
else
_e -> {:error, :notfound}
end
end
@impl true
def filter(%{"actor" => actor} = message) do
with {:ok, match, subchain} <- lookup_subchain(actor) do
Logger.debug(
"[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{
inspect(subchain)
}"
)
subchain
|> MRF.filter(message)
else
_e -> {:ok, message}
end
end
@impl true
def filter(message), do: {:ok, message}
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
@ -16,8 +17,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@moduledoc """
ActivityPub outgoing federation module.
"""
@ -63,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
with {:ok, %{status: code}} when code in 200..299 <-
result =
@httpoison.post(
HTTP.post(
inbox,
json,
[
@ -89,7 +88,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
!Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
end

View file

@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_likes
|> fix_addressing
|> fix_summary
|> fix_type
end
def fix_summary(%{"summary" => nil} = object) do
@ -65,7 +66,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
def fix_explicit_addressing(
%{"to" => to, "cc" => cc} = object,
explicit_mentions,
follower_collection
) do
explicit_to =
to
|> Enum.filter(fn x -> x in explicit_mentions end)
@ -76,6 +81,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
final_cc =
(cc ++ explicit_cc)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
@ -83,7 +89,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("cc", final_cc)
end
def fix_explicit_addressing(object, _explicit_mentions), do: object
def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
@ -93,10 +99,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
|> Utils.determine_explicit_mentions()
explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
object
|> fix_explicit_addressing(explicit_mentions)
explicit_mentions =
explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
@ -133,7 +141,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing
|> fix_explicit_addressing()
|> fix_implicit_addressing(followers_collection)
end
@ -328,6 +336,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_content_map(object), do: object
def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do
reply = Object.normalize(reply_id)
if reply && (reply.data["type"] == "Question" and object["name"]) do
Map.put(object, "type", "Answer")
else
object
end
end
def fix_type(object), do: object
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
@ -398,7 +418,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note", "Video", "Page"] do
when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
actor = Containment.get_actor(data)
data =
@ -438,10 +458,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
{_, false} <-
{:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{:user_locked, false} <- {:user_locked, User.locked?(followed)},
{:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
@ -450,7 +472,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -460,7 +482,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
@ -486,21 +508,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false
}) do
if not User.following?(follower, followed) do
{:ok, _follower} = User.follow(follower, followed)
end
{:ok, activity}
{:ok, _follower} = User.follow(follower, followed) do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed,
object: follow_activity.data["id"],
local: false
})
else
_e -> :error
end
@ -512,7 +529,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with actor <- Containment.get_actor(data),
{:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.reject(%{
@ -731,6 +748,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
|> set_type
end
# @doc
@ -895,6 +913,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "sensitive", "nsfw" in tags)
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger
@supported_object_types ["Article", "Note", "Video", "Page"]
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
@ -376,8 +376,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Updates a follow activity's state (for locked accounts).
"""
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
try do
@ -789,4 +789,22 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
query =
from(
[activity, object: object] in Activity.with_preloaded_object(Activity),
where: fragment("(?)->>'type' = 'Create'", activity.data),
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
where:
fragment(
"(?)->>'inReplyTo' = ?",
object.data,
^to_string(id)
),
where: fragment("(?)->>'type' = 'Answer'", object.data)
)
Repo.all(query)
end
end

View file

@ -66,6 +66,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private"
object.data["directMessage"] == true ->
"direct"
length(cc) > 0 ->
"private"

View file

@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
@ -362,6 +364,41 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
def config_show(conn, _params) do
configs = Pleroma.Repo.all(Config)
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: configs})
end
def config_update(conn, %{"configs" => configs}) do
updated =
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
updated =
Enum.map(configs, fn
%{"key" => key, "value" => value} ->
{:ok, config} = Config.update_or_create(%{key: key, value: value})
config
%{"key" => key, "delete" => "true"} ->
{:ok, _} = Config.delete(key)
nil
end)
|> Enum.reject(&is_nil(&1))
Pleroma.Config.TransferTask.load_and_update_env()
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env)])
updated
else
[]
end
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
end
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)

View file

@ -0,0 +1,144 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Config do
use Ecto.Schema
import Ecto.Changeset
alias __MODULE__
alias Pleroma.Repo
@type t :: %__MODULE__{}
schema "config" do
field(:key, :string)
field(:value, :binary)
timestamps()
end
@spec get_by_key(String.t()) :: Config.t() | nil
def get_by_key(key), do: Repo.get_by(Config, key: key)
@spec changeset(Config.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :value])
|> validate_required([:key, :value])
|> unique_constraint(:key)
end
@spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def create(%{key: key, value: value}) do
%Config{}
|> changeset(%{key: key, value: transform(value)})
|> Repo.insert()
end
@spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()}
def update(%Config{} = config, %{value: value}) do
config
|> change(value: transform(value))
|> Repo.update()
end
@spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def update_or_create(%{key: key} = params) do
with %Config{} = config <- Config.get_by_key(key) do
Config.update(config, params)
else
nil -> Config.create(params)
end
end
@spec delete(String.t()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def delete(key) do
with %Config{} = config <- Config.get_by_key(key) do
Repo.delete(config)
else
nil -> {:error, "Config with key #{key} not found"}
end
end
@spec from_binary(binary()) :: term()
def from_binary(value), do: :erlang.binary_to_term(value)
@spec from_binary_to_map(binary()) :: any()
def from_binary_to_map(binary) do
from_binary(binary)
|> do_convert()
end
defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1,
do: %{k => do_convert(v)}
defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val))
defp do_convert({k, v} = value) when is_tuple(value),
do: %{k => do_convert(v)}
defp do_convert(value) when is_binary(value) or is_atom(value) or is_map(value),
do: value
@spec transform(any()) :: binary()
def transform(entity) when is_map(entity) do
tuples =
for {k, v} <- entity,
into: [],
do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)}
Enum.reject(tuples, fn {_k, v} -> is_nil(v) end)
|> Enum.sort()
|> :erlang.term_to_binary()
end
def transform(entity) when is_list(entity) do
list = Enum.map(entity, &do_transform(&1))
:erlang.term_to_binary(list)
end
def transform(entity), do: :erlang.term_to_binary(entity)
defp do_transform(%Regex{} = value) when is_map(value), do: value
defp do_transform(value) when is_map(value) do
values =
for {key, val} <- value,
into: [],
do: {String.to_atom(key), do_transform(val)}
Enum.sort(values)
end
defp do_transform(value) when is_list(value) do
Enum.map(value, &do_transform(&1))
end
defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity)
defp do_transform(value) when is_binary(value) do
value = String.trim(value)
case String.length(value) do
0 ->
nil
_ ->
cond do
String.starts_with?(value, "Pleroma") ->
String.to_existing_atom("Elixir." <> value)
String.starts_with?(value, ":") ->
String.replace(value, ":", "") |> String.to_existing_atom()
String.starts_with?(value, "i:") ->
String.replace(value, "i:", "") |> String.to_integer()
true ->
value
end
end
end
defp do_transform(value), do: value
end

View file

@ -0,0 +1,16 @@
defmodule Pleroma.Web.AdminAPI.ConfigView do
use Pleroma.Web, :view
def render("index.json", %{configs: configs}) do
%{
configs: render_many(configs, __MODULE__, "show.json", as: :config)
}
end
def render("show.json", %{config: config}) do
%{
key: config.key,
value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value)
}
end
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
@ -23,6 +24,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
content =
unless is_nil(report.data["content"]) do
HTML.filter_tags(report.data["content"])
else
nil
end
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
@ -32,7 +40,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
id: report.id,
account: AccountView.render("account.json", %{user: account}),
actor: AccountView.render("account.json", %{user: user}),
content: report.data["content"],
content: content,
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"]

View file

@ -24,6 +24,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
end
end
@doc """
Gets or creates Pleroma.Registration record from Ueberauth assigns.
Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response
see [`docs/config.md`](docs/config.md).
"""
def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}),
do: {:error, :missing_uid}
def get_registration(%Plug.Conn{
assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth}
}) do
@ -51,9 +59,10 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials}
@doc "Creates Pleroma.User record basing on params and Pleroma.Registration record."
def create_from_registration(
%Plug.Conn{params: %{"authorization" => registration_attrs}},
registration
%Registration{} = registration
) do
nickname = value([registration_attrs["nickname"], Registration.nickname(registration)])
email = value([registration_attrs["email"], Registration.email(registration)])

View file

@ -35,9 +35,9 @@ defmodule Pleroma.Web.CommonAPI do
end
def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.maybe_follow(follower, followed),
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
@ -51,7 +51,7 @@ defmodule Pleroma.Web.CommonAPI do
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
@ -119,6 +119,56 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def vote(user, object, choices) do
with "Question" <- object.data["type"],
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
{options, max_count} <- get_options_and_max_count(object),
option_count <- Enum.count(options),
{:choice_check, {choices, true}} <-
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
{:ok, activity} =
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
activity
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
else
{:author, _} -> {:error, "Poll's author can't vote"}
{:existing_votes, _} -> {:error, "Already voted"}
{:choice_check, {_, false}} -> {:error, "Invalid indices"}
{:count_check, false} -> {:error, "Too many choices"}
end
end
defp get_options_and_max_count(object) do
if Map.has_key?(object.data, "anyOf") do
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
else
{object.data["oneOf"], 1}
end
end
defp normalize_and_validate_choice_indices(choices, count) do
Enum.map_reduce(choices, true, fn index, valid ->
index = if is_binary(index), do: String.to_integer(index), else: index
{index, if(valid, do: index < count, else: valid)}
end)
end
def get_visibility(%{"visibility" => visibility}, in_reply_to)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
@ -154,12 +204,15 @@ defmodule Pleroma.Web.CommonAPI do
data,
visibility
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
full_payload <- String.trim(status <> cw),
length when length in 1..limit <- String.length(full_payload),
:ok <- validate_character_limit(full_payload, attachments, limit),
object <-
make_note_data(
user.ap_id,
@ -171,13 +224,14 @@ defmodule Pleroma.Web.CommonAPI do
tags,
cw,
cc,
sensitive
sensitive,
poll
),
object <-
Map.put(
object,
"emoji",
Formatter.get_emoji_map(full_payload)
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
) do
res =
ActivityPub.create(
@ -193,6 +247,7 @@ defmodule Pleroma.Web.CommonAPI do
res
else
{:error, _} = e -> e
e -> {:error, e}
end
end

View file

@ -61,9 +61,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end)
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
@spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
{list(String.t()), list(String.t())}
def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
cc = [user.follower_address]
@ -74,9 +74,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
to = [user.follower_address | mentioned_users]
cc = ["https://www.w3.org/ns/activitystreams#Public"]
@ -87,14 +85,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
{[user.follower_address | to], cc}
end
def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
@ -102,6 +98,78 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
end
def get_addressed_users(mentioned_users, _), do: mentioned_users
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do
%{max_expiration: max_expiration, min_expiration: min_expiration} =
limits = Pleroma.Config.get([:instance, :poll_limits])
# XXX: There is probably a cleaner way of doing this
try do
# In some cases mastofe sends out strings instead of integers
expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in
if Enum.count(options) > limits.max_options do
raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
end
{poll, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
if String.length(option) > limits.max_option_chars do
raise ArgumentError,
message:
"Poll options cannot be longer than #{limits.max_option_chars} characters each"
end
{%{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
end)
case expires_in do
expires_in when expires_in > max_expiration ->
raise ArgumentError, message: "Expiration date is too far in the future"
expires_in when expires_in < min_expiration ->
raise ArgumentError, message: "Expiration date is too soon"
_ ->
:noop
end
end_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.to_iso8601()
poll =
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
%{"type" => "Question", "anyOf" => poll, "closed" => end_time}
else
%{"type" => "Question", "oneOf" => poll, "closed" => end_time}
end
{poll, emoji}
rescue
e in ArgumentError -> e.message
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
"Invalid poll"
end
def make_poll_data(_data) do
{%{}, %{}}
end
def make_content_html(
status,
attachments,
@ -224,7 +292,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
tags,
cw \\ nil,
cc \\ [],
sensitive \\ false
sensitive \\ false,
merge \\ %{}
) do
object = %{
"type" => "Note",
@ -239,12 +308,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
object =
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
Map.merge(object, merge)
end
def format_naive_asctime(date) do
@ -421,4 +493,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{:error, "No such conversation"}
end
end
def make_answer_data(%User{ap_id: ap_id}, object, name) do
%{
"type" => "Answer",
"actor" => ap_id,
"cc" => [object.data["actor"]],
"to" => [],
"name" => name,
"inReplyTo" => object.data["id"]
}
end
def validate_character_limit(full_payload, attachments, limit) do
length = String.length(full_payload)
if length < limit do
if length > 0 or Enum.count(attachments) > 0 do
:ok
else
{:error, "Cannot post an empty status without attachments"}
end
else
{:error, "The status is over the character limit"}
end
end
end

View file

@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do
|> put_status(status)
|> json(json)
end
@spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
def fetch_integer_param(params, name, default \\ nil) do
params
|> Map.get(name, default)
|> param_to_integer(default)
end
defp param_to_integer(val, _) when is_integer(val), do: val
defp param_to_integer(val, default) when is_binary(val) do
case Integer.parse(val) do
{res, _} -> res
_ -> default
end
end
defp param_to_integer(_, default), do: default
end

View file

@ -16,17 +16,32 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.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
plug(Pleroma.Plugs.InstanceStatic, at: "/")
# Cache-control headers are duplicated in case we turn off etags in the future
plug(Pleroma.Plugs.InstanceStatic,
at: "/",
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(
Plug.Static,
at: "/",
from: :pleroma,
only:
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
cache_control_for_etags: @static_cache_control,
headers: %{
"cache-control" => @static_cache_control
}
)
plug(Plug.Static.IndexHtml, at: "/pleroma/admin/")
@ -51,7 +66,7 @@ defmodule Pleroma.Web.Endpoint do
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason,
length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit),
length: Pleroma.Config.get([:instance, :upload_limit]),
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
)
@ -76,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do
Plug.Session,
store: :cookie,
key: cookie_name,
signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]},
signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
http_only: true,
secure: secure_cookies,
extra: extra

View file

@ -11,13 +11,11 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub
require Logger
@websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus)
def init do
# 1 minute
Process.sleep(1000 * 60)
@ -87,12 +85,12 @@ defmodule Pleroma.Web.Federator do
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
end)
@websub.verify(websub)
Websub.verify(websub)
end
def perform(:incoming_doc, doc) do
Logger.info("Got document, trying to parse")
@ostatus.handle_incoming(doc)
OStatus.handle_incoming(doc)
end
def perform(:incoming_ap_doc, params) do

View file

@ -15,7 +15,9 @@ defmodule Pleroma.Web.Federator.RetryQueue do
def start_link do
enabled =
if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false)
if Pleroma.Config.get(:env) == :test,
do: true,
else: Pleroma.Config.get([__MODULE__, :enabled], false)
if enabled do
Logger.info("Starting retry queue")
@ -219,7 +221,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do
{:noreply, state}
end
if Mix.env() == :test do
if Pleroma.Config.get(:env) == :test do
defp growth_function(_retries) do
_shutit = Pleroma.Config.get([__MODULE__, :initial_timeout])
DateTime.to_unix(DateTime.utc_now()) - 1

View file

@ -11,9 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Formatter
alias Pleroma.HTTP
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
@ -46,16 +46,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
require Logger
plug(
Pleroma.Plugs.RateLimitPlug,
%{
max_requests: Config.get([:app_account_creation, :max_requests]),
interval: Config.get([:app_account_creation, :interval])
}
when action in [:account_register]
)
plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
@httpoison Application.get_env(:pleroma, :httpoison)
@local_mastodon_name "Mastodon-Local"
action_fallback(:errors)
@ -117,13 +110,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Enum.dedup()
info_params =
[:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
[
:no_rich_text,
:locked,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)}
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
end)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
@ -132,6 +136,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> :error
end
end)
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :background) do
{:ok, object.data}
else
_ -> :error
end
end)
|> Map.put(:emoji, user_info_emojis)
info_cng = User.Info.profile_update(user.info, info_params)
@ -143,7 +155,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
CommonAPI.update(user)
end
json(conn, AccountView.render("account.json", %{user: user, for: user}))
json(
conn,
AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
)
else
_e ->
conn
@ -216,7 +231,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user, for: user})
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
account =
AccountView.render("account.json", %{
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
})
json(conn, account)
end
@ -260,7 +284,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
languages: ["en"],
registrations: Pleroma.Config.get([:instance, :registrations_open]),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit)
max_toot_chars: Keyword.get(instance, :limit),
poll_limits: Keyword.get(instance, :poll_limits)
}
json(conn, response)
@ -472,6 +497,67 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Object{} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
nil ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
false ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
end
end
defp get_cached_vote_or_vote(user, object, choices) do
idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
{_, res} =
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
case CommonAPI.vote(user, object, choices) do
{:error, _message} = res -> {:ignore, res}
res -> {:commit, res}
end
end)
res
end
def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
with %Object{} = object <- Object.get_by_id(id),
true <- object.data["type"] == "Question",
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
conn
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
nil ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
false ->
conn
|> put_status(404)
|> json(%{error: "Record not found"})
{:error, message} ->
conn
|> put_status(422)
|> json(%{error: message})
end
end
def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
@ -521,26 +607,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
params
|> Map.put("status", ".")
post_status(conn, params)
end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params =
params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
idempotency_key =
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> Ecto.UUID.generate()
end
scheduled_at = params["scheduled_at"]
if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
@ -553,17 +624,40 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
else
params = Map.drop(params, ["scheduled_at"])
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
CommonAPI.post(user, params)
end)
case get_cached_status_or_post(conn, params) do
{:ignore, message} ->
conn
|> put_status(422)
|> json(%{error: message})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
{:error, message} ->
conn
|> put_status(422)
|> json(%{error: message})
{_, activity} ->
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
end
defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
idempotency_key =
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> Ecto.UUID.generate()
end
Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
case CommonAPI.post(user, params) do
{:ok, activity} -> activity
{:error, message} -> {:ignore, message}
end
end)
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
@ -1107,114 +1201,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def status_search_query_with_gin(q, query) do
from([a, o] in q,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^query
),
order_by: [desc: :id]
)
end
def status_search_query_with_rum(q, query) do
from([a, o] in q,
where:
fragment(
"? @@ plainto_tsquery('english', ?)",
o.fts_content,
^query
),
order_by: [fragment("? <=> now()::date", o.inserted_at)]
)
end
def status_search(user, query) do
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, object} <- Fetcher.fetch_object_from_id(query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
[activity]
else
_e -> []
end
end || []
q =
from([a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
limit: 20
)
q =
if Pleroma.Config.get([:database, :rum_enabled]) do
status_search_query_with_rum(q, query)
else
status_search_query_with_gin(q, query)
end
Repo.all(q) ++ fetched
end
def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query)
tags_path = Web.base_url() <> "/tag/"
tags =
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
|> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query)
tags =
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
@ -1409,8 +1395,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
flavour = get_user_flavour(user)
initial_state =
%{
meta: %{
@ -1429,6 +1413,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
max_toot_chars: limit,
mascot: User.get_mascot(user)["url"]
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
delete_others_notice: present?(user.info.is_moderator),
admin: present?(user.info.is_admin)
@ -1496,7 +1481,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conn
|> put_layout(false)
|> put_view(MastodonView)
|> render("index.html", %{initial_state: initial_state, flavour: flavour})
|> render("index.html", %{initial_state: initial_state})
else
conn
|> put_session(:return_to, conn.request_path)
@ -1519,43 +1504,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
@supported_flavours ["glitch", "vanilla"]
def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
when flavour in @supported_flavours do
flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
with changeset <- Ecto.Changeset.change(user),
changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
{:ok, user} <- User.update_and_set_cache(changeset),
flavour <- user.info.flavour do
json(conn, flavour)
else
e ->
conn
|> put_resp_content_type("application/json")
|> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
end
end
def set_flavour(conn, _params) do
conn
|> put_status(400)
|> json(%{error: "Unsupported flavour"})
end
def get_flavour(%{assigns: %{user: user}} = conn, _params) do
json(conn, get_user_flavour(user))
end
defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
flavour
end
defp get_user_flavour(_) do
"glitch"
end
def login(%{assigns: %{user: %User{}}} = conn, _params) do
redirect(conn, to: local_mastodon_root_path(conn))
end
@ -1754,7 +1702,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
@httpoison.get(
HTTP.get(
url,
[],
adapter: [

View file

@ -0,0 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.ControllerHelper
require Logger
plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
statuses = Activity.search(user, query)
tags_path = Web.base_url() <> "/tag/"
tags =
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
|> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
statuses = Activity.search(user, query)
tags =
query
|> String.split()
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
defp search_options(params, user) do
[
resolve: params["resolve"] == "true",
following: params["following"] == "true",
limit: ControllerHelper.fetch_integer_param(params, "limit"),
offset: ControllerHelper.fetch_integer_param(params, "offset"),
for_user: user
]
end
end

View file

@ -66,6 +66,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
defp do_render("account.json", %{user: user} = opts) do
display_name = HTML.strip_tags(user.name || user.nickname)
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.get_cached_user_info(user)
@ -96,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(user.id),
username: username_from_nickname(user.nickname),
acct: user.nickname,
display_name: user.name || user.nickname,
display_name: display_name,
locked: user_info.locked,
created_at: Utils.to_masto_date(user.inserted_at),
followers_count: user_info.follower_count,
@ -124,12 +126,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites,
relationship: relationship
relationship: relationship,
skip_thread_containment: user.info.skip_thread_containment,
background_image: image_url(user.info.background) |> MediaProxy.url()
}
}
|> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info)
|> maybe_put_notification_settings(user, opts[:for])
|> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
end
defp username_from_nickname(string) when is_binary(string) do
@ -152,6 +158,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_settings(data, _, _, _), do: data
defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{
with_pleroma_settings: true
}) do
data
|> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store)
end
defp maybe_put_settings_store(data, _, _, _), do: data
defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{
with_chat_token: token
}) do
data
|> Kernel.put_in([:pleroma, :chat_token], token)
end
defp maybe_put_chat_token(data, _, _, _), do: data
defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
@ -171,4 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
defp maybe_put_notification_settings(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end

View file

@ -22,9 +22,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
last_status = StatusView.render("status.json", %{activity: activity, for: user})
# Conversations return all users except the current user.
users =
participation.conversation.users
|> Enum.reject(&(&1.id == user.id))
accounts =
AccountView.render("accounts.json", %{
users: participation.conversation.users,
users: users,
as: :user
})

View file

@ -240,6 +240,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
spoiler_text: summary_html,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render("poll.json", %{object: object, for: opts[:for]}),
mentions: mentions,
tags: build_tags(tags),
application: %{
@ -290,8 +291,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url |> MediaProxy.url(),
title: rich_media[:title],
description: rich_media[:description],
title: rich_media[:title] || "",
description: rich_media[:description] || "",
pleroma: %{
opengraph: rich_media
}
@ -329,6 +330,64 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
def render("poll.json", %{object: object} = opts) do
{multiple, options} =
case object.data do
%{"anyOf" => options} when is_list(options) -> {true, options}
%{"oneOf" => options} when is_list(options) -> {false, options}
_ -> {nil, nil}
end
if options do
end_time =
(object.data["closed"] || object.data["endTime"])
|> NaiveDateTime.from_iso8601!()
expired =
end_time
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|> case do
:lt -> true
_ -> false
end
voted =
if opts[:for] do
existing_votes =
Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
existing_votes != [] or opts[:for].ap_id == object.data["actor"]
else
false
end
{options, votes_count} =
Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
current_count = option["replies"]["totalItems"] || 0
{%{
title: HTML.strip_tags(name),
votes_count: current_count
}, current_count + count}
end)
%{
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: object.id,
expires_at: Utils.to_masto_date(end_time),
expired: expired,
multiple: multiple,
votes_count: votes_count,
options: options,
voted: voted,
emojis: build_emojis(object.data["emoji"])
}
else
nil
end
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
"public:media",
"public:local:media",
"user",
"user:notification",
"direct",
"list",
"hashtag"

View file

@ -12,25 +12,27 @@ defmodule Pleroma.Web.MediaProxy do
def url("/" <> _ = url), do: url
def url(url) do
config = Application.get_env(:pleroma, :media_proxy, [])
domain = URI.parse(url).host
cond do
!Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->
url
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
String.equivalent?(domain, pattern)
end) ->
url
true ->
encode_url(url)
if !enabled?() or local?(url) or whitelisted?(url) do
url
else
encode_url(url)
end
end
defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false)
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
defp whitelisted?(url) do
%{host: domain} = URI.parse(url)
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
String.equivalent?(domain, pattern)
end)
end
def encode_url(url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
@ -52,7 +54,7 @@ defmodule Pleroma.Web.MediaProxy do
end
def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
sig = Base.url_decode64!(sig, @base64_opts)
local_sig = :crypto.hmac(:sha, secret, url)

View file

@ -12,8 +12,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.Federator.Publisher
plug(Pleroma.Web.FederatingPlug)
def schemas(conn, _params) do
response = %{
links: [
@ -34,20 +32,15 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
# under software.
def raw_nodeinfo do
instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
chat = Application.get_env(:pleroma, :chat)
gopher = Application.get_env(:pleroma, :gopher)
stats = Stats.get_stats()
mrf_simple =
Application.get_env(:pleroma, :mrf_simple)
Config.get(:mrf_simple)
|> Enum.into(%{})
# This horror is needed to convert regex sigils to strings
mrf_keyword =
Application.get_env(:pleroma, :mrf_keyword, [])
Config.get(:mrf_keyword, [])
|> Enum.map(fn {key, value} ->
{key,
Enum.map(value, fn
@ -76,14 +69,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
quarantined = Keyword.get(instance, :quarantined_instances)
quarantined =
if is_list(quarantined) do
quarantined
else
[]
end
quarantined = Config.get([:instance, :quarantined_instances], [])
staff_accounts =
User.all_superusers()
@ -94,7 +80,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
federation_response =
if Keyword.get(instance, :mrf_transparency) do
if Config.get([:instance, :mrf_transparency]) do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
@ -111,22 +97,24 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
if Keyword.get(media_proxy, :enabled) do
"polls",
"pleroma_explicit_addressing",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
if Keyword.get(gopher, :enabled) do
if Config.get([:gopher, :enabled]) do
"gopher"
end,
if Keyword.get(chat, :enabled) do
if Config.get([:chat, :enabled]) do
"chat"
end,
if Keyword.get(suggestions, :enabled) do
if Config.get([:suggestions, :enabled]) do
"suggestions"
end,
if Keyword.get(instance, :allow_relay) do
if Config.get([:instance, :allow_relay]) do
"relay"
end,
if Keyword.get(instance, :safe_dm_mentions) do
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end
]
@ -143,7 +131,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
inbound: [],
outbound: []
},
openRegistrations: Keyword.get(instance, :registrations_open),
openRegistrations: Config.get([:instance, :registrations_open]),
usage: %{
users: %{
total: stats.user_count || 0
@ -151,29 +139,30 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
localPosts: stats.status_count || 0
},
metadata: %{
nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description),
private: !Keyword.get(instance, :public, true),
nodeName: Config.get([:instance, :name]),
nodeDescription: Config.get([:instance, :description]),
private: !Config.get([:instance, :public], true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""),
timeout: Keyword.get(suggestions, :timeout, 5000),
limit: Keyword.get(suggestions, :limit, 23),
web: Keyword.get(suggestions, :web, "")
enabled: Config.get([:suggestions, :enabled], false),
thirdPartyEngine: Config.get([:suggestions, :third_party_engine], ""),
timeout: Config.get([:suggestions, :timeout], 5000),
limit: Config.get([:suggestions, :limit], 23),
web: Config.get([:suggestions, :web], "")
},
staffAccounts: staff_accounts,
federation: federation_response,
postFormats: Keyword.get(instance, :allowed_post_formats),
pollLimits: Config.get([:instance, :poll_limits]),
postFormats: Config.get([:instance, :allowed_post_formats]),
uploadLimits: %{
general: Keyword.get(instance, :upload_limit),
avatar: Keyword.get(instance, :avatar_upload_limit),
banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit)
general: Config.get([:instance, :upload_limit]),
avatar: Config.get([:instance, :avatar_upload_limit]),
banner: Config.get([:instance, :banner_upload_limit]),
background: Config.get([:instance, :background_upload_limit])
},
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
accountActivationRequired: Config.get([:instance, :account_activation_required], false),
invitesEnabled: Config.get([:instance, :invites_enabled], false),
features: features,
restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames])
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames])
}
}
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Helpers.UriHelper
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
@ -17,6 +18,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken
alias Pleroma.Web.OAuth.Scopes
require Logger
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth)
plug(:fetch_session)
@ -24,34 +27,25 @@ defmodule Pleroma.Web.OAuth.OAuthController do
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(conn, %{"authorization" => _} = params) do
def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do
if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
else
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
app = Repo.preload(token, :app).app
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
redirect(conn, external: redirect_uri(conn, redirect_uri))
handle_existing_authorization(conn, params)
end
end
def authorize(conn, params), do: do_authorize(conn, params)
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
defp do_authorize(conn, params) do
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
@ -68,8 +62,41 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{"redirect_uri" => @oob_token_redirect_uri}
) do
render(conn, "oob_token_exists.html", %{token: token})
end
defp handle_existing_authorization(
%Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
%{} = params
) do
app = Repo.preload(token, :app).app
redirect_uri =
if is_binary(params["redirect_uri"]) do
params["redirect_uri"]
else
default_redirect_uri(app)
end
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{access_token: token.token}
url_params = UriHelper.append_param_if_present(url_params, :state, params["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, "Unlisted redirect_uri.")
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
def create_authorization(
conn,
%Plug.Conn{} = conn,
%{"authorization" => _} = params,
opts \\ []
) do
@ -81,35 +108,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
def after_create_authorization(conn, auth, %{
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => @oob_token_redirect_uri}
}) do
render(conn, "oob_authorization_created.html", %{auth: auth})
end
def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{
"authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs
}) do
redirect_uri = redirect_uri(conn, redirect_uri)
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
render(conn, "results.html", %{
auth: auth
})
else
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}
url_params =
if auth_attrs["state"] do
Map.put(url_params, :state, auth_attrs["state"])
else
url_params
end
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
app = Repo.preload(auth, :app).app
# An extra safety measure before we redirect (also done in `do_create_authorization/2`)
if redirect_uri in String.split(app.redirect_uris) do
redirect_uri = redirect_uri(conn, redirect_uri)
url_params = %{code: auth.token}
url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"])
url = UriHelper.append_uri_params(redirect_uri, url_params)
redirect(conn, external: url)
else
conn
|> put_flash(:error, "Unlisted redirect_uri.")
|> redirect(external: redirect_uri(conn, redirect_uri))
end
end
defp handle_create_authorization_error(
conn,
%Plug.Conn{} = conn,
{:error, scopes_issue},
%{"authorization" => _} = params
)
@ -123,7 +148,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
defp handle_create_authorization_error(
conn,
%Plug.Conn{} = conn,
{:auth_active, false},
%{"authorization" => _} = params
) do
@ -135,13 +160,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|> authorize(params)
end
defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do
defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do
Authenticator.handle_error(conn, error)
end
@doc "Renew access_token with refresh_token"
def token_exchange(
conn,
%Plug.Conn{} = conn,
%{"grant_type" => "refresh_token", "refresh_token" => token} = _params
) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
@ -157,7 +182,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
@ -174,7 +199,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
def token_exchange(
conn,
%Plug.Conn{} = conn,
%{"grant_type" => "password"} = params
) do
with {:ok, %User{} = user} <- Authenticator.get_user(conn),
@ -205,7 +230,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
def token_exchange(
conn,
%Plug.Conn{} = conn,
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
@ -216,7 +241,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
token_exchange(conn, params)
end
def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do
def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, auth} <- Authorization.create_authorization(app, %User{}),
{:ok, token} <- Token.exchange_token(app, auth) do
@ -229,9 +254,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
# Bad request
def token_exchange(conn, params), do: bad_request(conn, params)
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def token_revoke(conn, %{"token" => _token} = params) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do
with {:ok, app} <- Token.Utils.fetch_app(conn),
{:ok, _token} <- RevokeToken.revoke(app, params) do
json(conn, %{})
@ -242,17 +267,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
def token_revoke(conn, params), do: bad_request(conn, params)
def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
# Response for bad request
defp bad_request(conn, _) do
defp bad_request(%Plug.Conn{} = conn, _) do
conn
|> put_status(500)
|> json(%{error: "Bad request"})
end
@doc "Prepares OAuth request to provider for Ueberauth"
def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do
def prepare_request(%Plug.Conn{} = conn, %{
"provider" => provider,
"authorization" => auth_attrs
}) do
scope =
auth_attrs
|> Scopes.fetch_scopes([])
@ -273,7 +301,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
redirect(conn, to: o_auth_path(conn, :request, provider, params))
end
def request(conn, params) do
def request(%Plug.Conn{} = conn, params) do
message =
if params["provider"] do
"Unsupported OAuth provider: #{params["provider"]}."
@ -286,7 +314,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|> redirect(to: "/")
end
def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do
def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do
params = callback_params(params)
messages = for e <- Map.get(failure, :errors, []), do: e.message
message = Enum.join(messages, "; ")
@ -296,7 +324,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
end
def callback(conn, params) do
def callback(%Plug.Conn{} = conn, params) do
params = callback_params(params)
with {:ok, registration} <- Authenticator.get_registration(conn) do
@ -314,11 +342,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
conn
|> put_session(:registration_id, registration.id)
|> put_session_registration_id(registration.id)
|> registration_details(%{"authorization" => registration_params})
end
else
_ ->
error ->
Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns]))
conn
|> put_flash(:error, "Failed to set up user account.")
|> redirect(external: redirect_uri(conn, params["redirect_uri"]))
@ -329,7 +359,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
Map.merge(params, Jason.decode!(state))
end
def registration_details(conn, %{"authorization" => auth_attrs}) do
def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do
render(conn, "register.html", %{
client_id: auth_attrs["client_id"],
redirect_uri: auth_attrs["redirect_uri"],
@ -340,7 +370,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
})
end
def register(conn, %{"authorization" => _, "op" => "connect"} = params) do
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth}} <-
@ -359,7 +389,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
def register(conn, %{"authorization" => _, "op" => "register"} = params) do
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{:ok, user} <- Authenticator.create_from_registration(conn, registration) do
@ -395,7 +425,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
defp do_create_authorization(
conn,
%Plug.Conn{} = conn,
%{
"authorization" =>
%{
@ -416,13 +446,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
# Special case: Local MastodonFE
defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login)
defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login)
defp redirect_uri(_conn, redirect_uri), do: redirect_uri
defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri
defp get_session_registration_id(conn), do: get_session(conn, :registration_id)
defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id)
defp put_session_registration_id(conn, registration_id),
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id)
@spec validate_scopes(App.t(), map()) ::
@ -432,4 +462,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|> Scopes.fetch_scopes(app.scopes)
|> Scopes.validates(app.scopes)
end
def default_redirect_uri(%App{} = app) do
app.redirect_uris
|> String.split()
|> Enum.at(0)
end
end

View file

@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.Token.Query
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
@type t :: %__MODULE__{}
schema "oauth_tokens" do
@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do
defp put_valid_until(changeset, attrs) do
expires_in =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in))
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in()))
changeset
|> change(%{valid_until: expires_in})
@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do
end
def is_expired?(_), do: false
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end

View file

@ -4,15 +4,13 @@ defmodule Pleroma.Web.OAuth.Token.Response do
alias Pleroma.User
alias Pleroma.Web.OAuth.Token.Utils
@expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
@doc false
def build(%User{} = user, token, opts \\ %{}) do
%{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: @expires_in,
expires_in: expires_in(),
scope: Enum.join(token.scopes, " "),
me: user.ap_id
}
@ -25,8 +23,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do
access_token: token.token,
refresh_token: token.refresh_token,
created_at: Utils.format_created_at(token),
expires_in: @expires_in,
expires_in: expires_in(),
scope: Enum.join(token.scopes, " ")
}
end
defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600)
end

View file

@ -3,13 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus do
@httpoison Application.get_env(:pleroma, :httpoison)
import Ecto.Query
import Pleroma.Web.XML
require Logger
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@ -363,7 +362,7 @@ defmodule Pleroma.Web.OStatus do
def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
HTTP.get(
url,
[{:Accept, "application/atom+xml"}]
) do
@ -380,7 +379,7 @@ defmodule Pleroma.Web.OStatus do
Logger.debug("Trying to fetch #{url}")
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body}} <- @httpoison.get(url, []),
{:ok, %{body: body}} <- HTTP.get(url, []),
{:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url)
else

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.RelMe do
with_body: true
]
if Mix.env() == :test do
if Pleroma.Config.get(:env) == :test do
def parse(url) when is_binary(url), do: parse_url(url)
else
def parse(url) when is_binary(url) do

View file

@ -9,7 +9,9 @@ defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.Web.RichMedia.Parser
defp validate_page_url(page_url) when is_binary(page_url) do
if AutoLinker.Parser.is_url?(page_url, true) do
validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld]
if AutoLinker.Parser.url?(page_url, scheme: true, validate_tld: validate_tld) do
URI.parse(page_url) |> validate_page_url
else
:error

View file

@ -18,7 +18,7 @@ defmodule Pleroma.Web.RichMedia.Parser do
def parse(nil), do: {:error, "No URL provided"}
if Mix.env() == :test do
if Pleroma.Config.get(:env) == :test do
def parse(url), do: parse_url(url)
else
def parse(url) do
@ -37,7 +37,10 @@ defmodule Pleroma.Web.RichMedia.Parser do
try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options)
html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data()
html
|> maybe_parse()
|> clean_parsed_data()
|> check_parsed_data()
rescue
e ->
{:error, "Parsing error: #{inspect(e)}"}

View file

@ -202,6 +202,9 @@ defmodule Pleroma.Web.Router do
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
end
scope "/", Pleroma.Web.TwitterAPI do
@ -309,8 +312,6 @@ defmodule Pleroma.Web.Router do
post("/conversations/:id/read", MastodonAPIController, :conversation_read)
get("/endorsements", MastodonAPIController, :empty_array)
get("/pleroma/flavour", MastodonAPIController, :get_flavour)
end
scope [] do
@ -339,6 +340,8 @@ defmodule Pleroma.Web.Router do
put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
post("/polls/:id/votes", MastodonAPIController, :poll_vote)
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
@ -354,8 +357,6 @@ defmodule Pleroma.Web.Router do
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour)
get("/pleroma/mascot", MastodonAPIController, :get_mascot)
put("/pleroma/mascot", MastodonAPIController, :set_mascot)
@ -418,7 +419,7 @@ defmodule Pleroma.Web.Router do
get("/trends", MastodonAPIController, :empty_array)
get("/accounts/search", MastodonAPIController, :account_search)
get("/accounts/search", SearchController, :account_search)
scope [] do
pipe_through(:oauth_read_or_public)
@ -430,12 +431,14 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/polls/:id", MastodonAPIController, :get_poll)
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/search", MastodonAPIController, :search)
get("/search", SearchController, :search)
get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end
@ -443,7 +446,7 @@ defmodule Pleroma.Web.Router do
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through([:api, :oauth_read_or_public])
get("/search", MastodonAPIController, :search2)
get("/search", SearchController, :search2)
end
scope "/api", Pleroma.Web do
@ -608,12 +611,6 @@ defmodule Pleroma.Web.Router do
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
scope "/", Pleroma.Web do
pipe_through(:oembed)
get("/oembed", OEmbed.OEmbedController, :url)
end
pipeline :activitypub do
plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
@ -703,7 +700,7 @@ defmodule Pleroma.Web.Router do
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Mix.env() == :dev do
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])

View file

@ -5,11 +5,10 @@
defmodule Pleroma.Web.Salmon do
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Keys
alias Pleroma.User
@ -138,7 +137,7 @@ defmodule Pleroma.Web.Salmon do
def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
with {:ok, %{status: code}} when code in 200..299 <-
@httpoison.post(
HTTP.post(
url,
feed,
[{"Content-Type", "application/magic-envelope+xml"}]
@ -147,7 +146,7 @@ defmodule Pleroma.Web.Salmon do
do: Instances.set_reachable(url)
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
:ok
{:ok, code}
else
e ->
unless params[:unreachable_since], do: Instances.set_reachable(url)

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
@ -109,23 +110,18 @@ defmodule Pleroma.Web.Streamer do
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do
topic = "user:#{item.user_id}"
Enum.each(topics[topic] || [], fn socket ->
json =
%{
event: "notification",
payload:
NotificationView.render("show.json", %{
notification: item,
for: socket.assigns["user"]
})
|> Jason.encode!()
}
|> Jason.encode!()
send(socket.transport_pid, {:text, json})
def handle_cast(
%{action: :stream, topic: topic, item: %Notification{} = item},
topics
)
when topic in ["user", "user:notification"] do
topics
|> Map.get("#{topic}:#{item.user_id}", [])
|> Enum.each(fn socket ->
send(
socket.transport_pid,
{:text, represent_notification(socket.assigns[:user], item)}
)
end)
{:noreply, topics}
@ -215,6 +211,20 @@ defmodule Pleroma.Web.Streamer do
|> Jason.encode!()
end
@spec represent_notification(User.t(), Notification.t()) :: binary()
defp represent_notification(%User{} = user, %Notification{} = notify) do
%{
event: "notification",
payload:
NotificationView.render(
"show.json",
%{notification: notify, for: user}
)
|> Jason.encode!()
}
|> Jason.encode!()
end
def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc.
@ -224,11 +234,10 @@ defmodule Pleroma.Web.Streamer do
mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || []
parent = Object.normalize(item)
unless is_nil(parent) or item.actor in blocks or item.actor in mutes or
item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or
parent.data["actor"] in blocks or parent.data["actor"] in mutes do
with parent when not is_nil(parent) <- Object.normalize(item),
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
true <- thread_containment(item, user) do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
@ -264,8 +273,8 @@ defmodule Pleroma.Web.Streamer do
blocks = user.info.blocks || []
mutes = user.info.mutes || []
unless item.actor in blocks or item.actor in mutes or
not ActivityPub.contain_activity(item, user) do
with true <- Enum.all?([blocks, mutes], &(item.actor not in &1)),
true <- thread_containment(item, user) do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
else
@ -274,9 +283,20 @@ defmodule Pleroma.Web.Streamer do
end)
end
defp internal_topic(topic, socket) when topic in ~w[user direct] do
defp internal_topic(topic, socket) when topic in ~w[user user:notification direct] do
"#{topic}:#{socket.assigns[:user].id}"
end
defp internal_topic(topic, _), do: topic
@spec thread_containment(Activity.t(), User.t()) :: boolean()
defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true
defp thread_containment(activity, user) do
if Config.get([:instance, :skip_thread_containment]) do
true
else
ActivityPub.contain_activity(activity, user)
end
end
end

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" />
<title>
<%= Application.get_env(:pleroma, :instance)[:name] %>
<%= Pleroma.Config.get([:instance, :name]) %>
</title>
<style>
body {
@ -63,13 +63,14 @@
.scopes-input {
display: flex;
flex-direction: column;
margin-top: 1em;
text-align: left;
color: #89898a;
}
.scopes-input label:first-child {
flex-basis: 40%;
height: 2em;
}
.scopes {
@ -80,13 +81,22 @@
}
.scope {
flex-basis: 100%;
display: flex;
flex-basis: 100%;
height: 2em;
align-items: center;
}
.scope:before {
color: #b9b9ba;
content: "\fe0e";
margin-left: 1em;
margin-right: 1em;
}
[type="checkbox"] + label {
display: none;
cursor: pointer;
margin: 0.5em;
}
@ -95,10 +105,12 @@
}
[type="checkbox"] + label:before {
cursor: pointer;
display: inline-block;
color: white;
background-color: #121a24;
border: 4px solid #121a24;
box-shadow: 0px 0px 1px 0 #d8a070;
box-sizing: border-box;
width: 1.2em;
height: 1.2em;
@ -128,7 +140,8 @@
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 30px;
margin-top: 20px;
margin-bottom: 20px;
text-transform: uppercase;
font-size: 16px;
box-shadow: 0px 0px 2px 0px black,
@ -147,8 +160,8 @@
box-sizing: border-box;
width: 100%;
background-color: #931014;
border: 1px solid #a06060;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 20px;
font-weight: 500;
@ -171,12 +184,27 @@
margin-top: 0
}
.scopes-input {
flex-direction: column;
.scope {
flex-basis: 0%;
}
.scope {
flex-basis: 50%;
.scope:before {
content: "";
margin-left: 0em;
margin-right: 1em;
}
.scope:first-child:before {
margin-left: 1em;
content: "\fe0e";
}
.scope:after {
content: ",";
}
.scope:last-child:after {
content: "";
}
}
.form-row {
@ -194,7 +222,7 @@
</head>
<body>
<div class="container">
<h1><%= Application.get_env(:pleroma, :instance)[:name] %></h1>
<h1><%= Pleroma.Config.get([:instance, :name]) %></h1>
<%= render @view_module, @view_template, assigns %>
</div>
</body>

View file

@ -4,11 +4,11 @@
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<title>
<%= Application.get_env(:pleroma, :instance)[:name] %>
<%= Pleroma.Config.get([:instance, :name]) %>
</title>
<link rel="icon" type="image/png" href="/favicon.png"/>
<script crossorigin='anonymous' src="/packs/locales.js"></script>
<script crossorigin='anonymous' src="/packs/locales/<%= @flavour %>/en.js"></script>
<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'>
<link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'>
@ -19,10 +19,10 @@
<script src="/packs/core/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/core/common.css" />
<script src="/packs/flavours/<%= @flavour %>/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/flavours/<%= @flavour %>/common.css" />
<script src="/packs/flavours/glitch/common.js"></script>
<link rel="stylesheet" media="all" href="/packs/flavours/glitch/common.css" />
<script src="/packs/flavours/<%= @flavour %>/home.js"></script>
<script src="/packs/flavours/glitch/home.js"></script>
</head>
<body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>

View file

@ -1,13 +1,19 @@
<div class="scopes-input">
<%= label @form, :scope, "Permissions" %>
<%= label @form, :scope, "The following permissions will be granted" %>
<div class="scopes">
<%= for scope <- @available_scopes do %>
<%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %>
<div class="scope">
<%= if scope in @scopes do %>
<div class="scope">
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
<%= label @form, :"scope_#{scope}", String.capitalize(scope) %>
<%= if scope in @scopes && scope do %>
<%= String.capitalize(scope) %>
<% end %>
</div>
<% else %>
<%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %>
<%= label @form, :"scope_#{scope}", String.capitalize(scope) %>
</div>
<% end %>
<% end %>
</div>
</div>

View file

@ -1,7 +1,9 @@
<h2>Sign in with external provider</h2>
<%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %>
<%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %>
<div style="display: none">
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
</div>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>

View file

@ -0,0 +1,2 @@
<h1>Authorization exists</h1>
<h2>Access token is <%= @token.token %></h2>

View file

@ -6,26 +6,38 @@
<% end %>
<h2>OAuth Authorization</h2>
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
<div class="input">
<%= label f, :name, "Name or email" %>
<%= text_input f, :name %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<%= if @params["registration"] in ["true", true] do %>
<h3>This is the first time you visit! Please enter your Pleroma handle.</h3>
<p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p>
<div class="input">
<%= label f, :nickname, "Pleroma Handle" %>
<%= text_input f, :nickname, placeholder: "lain" %>
</div>
<%= hidden_input f, :name, value: @params["name"] %>
<%= hidden_input f, :password, value: @params["password"] %>
<br>
<% else %>
<div class="input">
<%= label f, :name, "Username" %>
<%= text_input f, :name %>
</div>
<div class="input">
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
</div>
<%= submit "Log In" %>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %>
<%= hidden_input f, :client_id, value: @client_id %>
<%= hidden_input f, :response_type, value: @response_type %>
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
<%= hidden_input f, :state, value: @state %>
<%= submit "Authorize" %>
<% end %>
<%= if Pleroma.Config.oauth_consumer_enabled?() do %>
<%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>
<% end %>

View file

@ -667,7 +667,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
defp build_info_cng(user, params) do
info_params =
["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
[
"no_rich_text",
"locked",
"hide_followers",
"hide_follows",
"hide_favorites",
"show_role",
"skip_thread_containment"
]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
@ -763,7 +771,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
def only_if_public_instance(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
if Pleroma.Config.get([:instance, :public]) do
conn
else
conn

View file

@ -118,9 +118,11 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"pleroma" =>
%{
"confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags
"tags" => user.tags,
"skip_thread_containment" => user.info.skip_thread_containment
}
|> maybe_with_activation_status(user, for_user)
|> with_notification_settings(user, for_user)
}
|> maybe_with_user_settings(user, for_user)
|> maybe_with_role(user, for_user)
@ -132,6 +134,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
end
end
defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
Map.put(data, "notification_settings", user.info.notification_settings)
end
defp with_notification_settings(data, _, _), do: data
defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do
Map.put(data, "deactivated", user.info.deactivated)
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.ErrorView do
def render("500.json", assigns) do
Logger.error("Internal server error: #{inspect(assigns[:reason])}")
if Mix.env() != :prod do
if Pleroma.Config.get(:env) != :prod do
%{errors: %{detail: "Internal server error", reason: inspect(assigns[:reason])}}
else
%{errors: %{detail: "Internal server error"}}

View file

@ -3,8 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger do
@httpoison Application.get_env(:pleroma, :httpoison)
alias Pleroma.HTTP
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.Federator.Publisher
@ -176,11 +175,11 @@ defmodule Pleroma.Web.WebFinger do
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
@httpoison.get("http://#{domain}/.well-known/host-meta", []) do
HTTP.get("http://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
with {:ok, %{body: body}} <- HTTP.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
@ -209,7 +208,7 @@ defmodule Pleroma.Web.WebFinger do
end
with response <-
@httpoison.get(
HTTP.get(
address,
Accept: "application/xrd+xml,application/jrd+json"
),

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.User
@ -24,9 +25,7 @@ defmodule Pleroma.Web.Websub do
@behaviour Pleroma.Web.Federator.Publisher
@httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do
def verify(subscription, getter \\ &HTTP.get/3) do
challenge = Base.encode16(:crypto.strong_rand_bytes(8))
lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
lease_seconds = lease_seconds |> to_string
@ -207,7 +206,7 @@ defmodule Pleroma.Web.Websub do
requester.(subscription)
end
def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
def gather_feed_data(topic, getter \\ &HTTP.get/1) do
with {:ok, response} <- getter.(topic),
status when status in 200..299 <- response.status,
body <- response.body,
@ -236,7 +235,7 @@ defmodule Pleroma.Web.Websub do
end
end
def request_subscription(websub, poster \\ &@httpoison.post/3, timeout \\ 10_000) do
def request_subscription(websub, poster \\ &HTTP.post/3, timeout \\ 10_000) do
data = [
"hub.mode": "subscribe",
"hub.topic": websub.topic,
@ -294,7 +293,7 @@ defmodule Pleroma.Web.Websub do
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status: code}} when code in 200..299 <-
@httpoison.post(
HTTP.post(
callback,
xml,
[