Merge branch 'develop' into issue/941

This commit is contained in:
Maksim Pechnikov 2019-06-04 09:49:08 +03:00
commit 4f2e359687
45 changed files with 1456 additions and 149 deletions

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

@ -127,10 +127,15 @@ defmodule Pleroma.Notification do
def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = get_notified_from_activity(activity)
object = Object.normalize(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
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(_), do: {:ok, []}
@ -166,7 +171,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 +193,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 +209,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

@ -61,8 +61,6 @@ defmodule Pleroma.ReverseProxy do
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Pleroma.Config.get(:hackney, :hackney)
@default_hackney_options []
@inline_content_types [
@ -148,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}
@ -198,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)),

View file

@ -44,9 +44,15 @@ defmodule Pleroma.User.Info do
field(:pinned_activities, {:array, :string}, default: [])
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)
@ -69,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}
@ -211,7 +222,8 @@ defmodule Pleroma.User.Info do
:hide_favorites,
:background,
:show_role,
:skip_thread_containment
:skip_thread_containment,
:pleroma_settings_store
])
end

View file

@ -109,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),
@ -184,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
@ -236,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),
@ -477,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(
@ -489,6 +502,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
^context
)
)
|> exclude_poll_votes(opts)
|> order_by([activity], desc: activity.id)
end
@ -496,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
@ -504,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()
@ -803,6 +816,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
@ -864,6 +889,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end
def fetch_activities(recipients, opts \\ %{}) do

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,6 +16,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
end)
end
def filter(%{} = object), do: get_policies() |> filter(object)
def get_policies do
Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()
end

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

@ -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
@ -335,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.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),
@ -405,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 =
@ -738,6 +751,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> strip_internal_fields
|> strip_internal_tags
|> set_type
end
# @doc
@ -902,6 +916,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)
@ -789,4 +789,21 @@ 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("(?)->>'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

@ -119,6 +119,53 @@ 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"])
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
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,6 +201,7 @@ defmodule Pleroma.Web.CommonAPI do
data,
visibility
),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"] || "",
@ -171,13 +219,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(

View file

@ -102,6 +102,72 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
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 +290,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
tags,
cw \\ nil,
cc \\ [],
sensitive \\ false
sensitive \\ false,
merge \\ %{}
) do
object = %{
"type" => "Note",
@ -239,12 +306,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 +491,15 @@ 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
end

View file

@ -132,6 +132,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
@ -151,7 +154,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
@ -161,7 +167,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user, for: user})
account =
AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
json(conn, account)
end
@ -205,7 +213,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)
@ -417,6 +426,53 @@ 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
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} <- CommonAPI.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
@ -480,12 +536,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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
@ -498,17 +548,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, %{})
@ -1372,6 +1445,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)

View file

@ -131,6 +131,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> 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)
end
defp username_from_nickname(string) when is_binary(string) do
@ -153,6 +154,15 @@ 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_role(data, %User{info: %{show_role: true}} = user, _) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)

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: %{
@ -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

@ -97,6 +97,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
"pleroma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
@ -149,6 +150,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
},
staffAccounts: staff_accounts,
federation: federation_response,
pollLimits: Config.get([:instance, :poll_limits]),
postFormats: Config.get([:instance, :allowed_post_formats]),
uploadLimits: %{
general: Config.get([:instance, :upload_limit]),

View file

@ -333,6 +333,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)
@ -422,6 +424,8 @@ 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)

View file

@ -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 {

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

@ -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

@ -122,6 +122,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"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)
@ -133,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