Merge branch 'develop' into feature/configurable-blocks

This commit is contained in:
squidboi 2018-06-16 15:37:16 -07:00
commit 2e294ee44a
51 changed files with 1066 additions and 167 deletions

72
CONFIGURATION.md Normal file
View file

@ -0,0 +1,72 @@
# Configuring Pleroma
In the `config/` directory, you will find the following relevant files:
* `config.exs`: default base configuration
* `dev.exs`: default additional configuration for `MIX_ENV=dev`
* `prod.exs`: default additional configuration for `MIX_ENV=prod`
Do not modify files in the list above.
Instead, overload the settings by editing the following files:
* `dev.secret.exs`: custom additional configuration for `MIX_ENV=dev`
* `prod.secret.exs`: custom additional configuration for `MIX_ENV=prod`
## Message Rewrite Filters (MRFs)
Modify incoming and outgoing posts.
config :pleroma, :instance,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy
`rewrite_policy` specifies which MRF policies to apply.
It can either be a single policy or a list of policies.
Currently, MRFs availible by default are:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`
Some policies, such as SimplePolicy and RejectNonPublic,
can be additionally configured in their respective sections.
### NoOpPolicy
Does not modify posts (this is the default `rewrite_policy`)
### DropPolicy
Drops all posts.
It generally does not make sense to use this in production.
### SimplePolicy
Restricts the visibility of posts from certain instances.
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
reject: []
* `media_removal`: posts from these instances will have attachments
removed
* `media_nsfw`: posts from these instances will have attachments marked
as nsfw
* `federated_timeline_removal`: posts from these instances will be
marked as unlisted
* `reject`: posts from these instances will be dropped
### RejectNonPublic
Drops posts with non-public visibility settings.
config :pleroma :mrf_rejectnonpublic
allow_followersonly: false,
allow_direct: false,
* `allow_followersonly`: whether to allow follower-only posts through
the filter
* `allow_direct`: whether to allow direct messages through the filter

View file

@ -64,6 +64,10 @@
config :pleroma, :user, deny_follow_blocked: true config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_simple, config :pleroma, :mrf_simple,
media_removal: [], media_removal: [],
media_nsfw: [], media_nsfw: [],

View file

@ -24,17 +24,26 @@ server {
# } # }
} }
# Enable SSL session caching for improved performance
ssl_session_cache shared:ssl_session_cache:10m;
server { server {
listen 443 ssl http2; listen 443 ssl http2;
ssl on;
ssl_session_timeout 5m; ssl_session_timeout 5m;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Add TLSv1.0 to support older devices
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; ssl_protocols TLSv1.2;
# Uncomment line below if you want to support older devices (Before Android 4.4.2, IE 8, etc.)
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_ecdh_curve X25519:prime256v1:secp384r1:secp521r1;
ssl_stapling on;
ssl_stapling_verify on;
server_name example.tld; server_name example.tld;

View file

@ -5,7 +5,7 @@ defmodule Mix.Tasks.SetModerator do
@shortdoc "Set moderator status" @shortdoc "Set moderator status"
def run([nickname | rest]) do def run([nickname | rest]) do
ensure_started(Repo, []) Application.ensure_all_started(:pleroma)
moderator = moderator =
case rest do case rest do
@ -19,7 +19,7 @@ def run([nickname | rest]) do
|> Map.put("is_moderator", !!moderator) |> Map.put("is_moderator", !!moderator)
cng = User.info_changeset(user, %{info: info}) cng = User.info_changeset(user, %{info: info})
user = Repo.update!(cng) {:ok, user} = User.update_and_set_cache(cng)
IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}") IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
else else

View file

@ -8,7 +8,8 @@ config :pleroma, :instance,
name: "<%= name %>", name: "<%= name %>",
email: "<%= email %>", email: "<%= email %>",
limit: 5000, limit: 5000,
registrations_open: true registrations_open: true,
dedupe_media: false
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,

View file

@ -0,0 +1,30 @@
defmodule Mix.Tasks.SetLocked do
use Mix.Task
import Mix.Ecto
alias Pleroma.{Repo, User}
@shortdoc "Set locked status"
def run([nickname | rest]) do
ensure_started(Repo, [])
locked =
case rest do
[locked] -> locked == "true"
_ -> true
end
with %User{local: true} = user <- User.get_by_nickname(nickname) do
info =
user.info
|> Map.put("locked", !!locked)
cng = User.info_changeset(user, %{info: info})
user = Repo.update!(cng)
IO.puts("locked status of #{nickname}: #{user.info["locked"]}")
else
_ ->
IO.puts("No local user #{nickname}")
end
end
end

View file

@ -1,7 +1,7 @@
defmodule Pleroma.List do defmodule Pleroma.List do
use Ecto.Schema use Ecto.Schema
import Ecto.{Changeset, Query} import Ecto.{Changeset, Query}
alias Pleroma.{User, Repo} alias Pleroma.{User, Repo, Activity}
schema "lists" do schema "lists" do
belongs_to(:user, Pleroma.User) belongs_to(:user, Pleroma.User)
@ -56,6 +56,19 @@ def get_following(%Pleroma.List{following: following} = list) do
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
end end
# Get lists the activity should be streamed to.
def get_lists_from_activity(%Activity{actor: ap_id}) do
actor = User.get_cached_by_ap_id(ap_id)
query =
from(
l in Pleroma.List,
where: fragment("? && ?", l.following, ^[actor.follower_address])
)
Repo.all(query)
end
def rename(%Pleroma.List{} = list, title) do def rename(%Pleroma.List{} = list, title) do
list list
|> title_changeset(%{title: title}) |> title_changeset(%{title: title})

View file

@ -2,19 +2,20 @@ defmodule Pleroma.Upload do
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Web alias Pleroma.Web
def store(%Plug.Upload{} = file) do def store(%Plug.Upload{} = file, should_dedupe) do
uuid = UUID.generate() content_type = get_content_type(file.path)
upload_folder = Path.join(upload_path(), uuid) uuid = get_uuid(file, should_dedupe)
File.mkdir_p!(upload_folder) name = get_name(file, uuid, content_type, should_dedupe)
result_file = Path.join(upload_folder, file.filename) upload_folder = get_upload_path(uuid, should_dedupe)
File.cp!(file.path, result_file) url_path = get_url(name, uuid, should_dedupe)
# fix content type on some image uploads File.mkdir_p!(upload_folder)
content_type = result_file = Path.join(upload_folder, name)
if file.content_type in [nil, "application/octet-stream"] do
get_content_type(file.path) if File.exists?(result_file) do
File.rm!(file.path)
else else
file.content_type File.cp!(file.path, result_file)
end end
%{ %{
@ -23,26 +24,48 @@ def store(%Plug.Upload{} = file) do
%{ %{
"type" => "Link", "type" => "Link",
"mediaType" => content_type, "mediaType" => content_type,
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename))) "href" => url_path
} }
], ],
"name" => file.filename, "name" => name
"uuid" => uuid
} }
end end
def store(%{"img" => "data:image/" <> image_data}) do def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"]) data = Base.decode64!(parsed["data"], ignore: :whitespace)
uuid = UUID.generate() uuid = UUID.generate()
upload_folder = Path.join(upload_path(), uuid) uuidpath = Path.join(upload_path(), uuid)
uuid = UUID.generate()
File.mkdir_p!(upload_path())
File.write!(uuidpath, data)
content_type = get_content_type(uuidpath)
name =
create_name(
String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
parsed["filetype"],
content_type
)
upload_folder = get_upload_path(uuid, should_dedupe)
url_path = get_url(name, uuid, should_dedupe)
File.mkdir_p!(upload_folder) File.mkdir_p!(upload_folder)
filename = Base.encode16(:crypto.hash(:sha256, data)) <> ".#{parsed["filetype"]}" result_file = Path.join(upload_folder, name)
result_file = Path.join(upload_folder, filename)
File.write!(result_file, data) if should_dedupe do
if !File.exists?(result_file) do
content_type = "image/#{parsed["filetype"]}" File.rename(uuidpath, result_file)
else
File.rm!(uuidpath)
end
else
File.rename(uuidpath, result_file)
end
%{ %{
"type" => "Image", "type" => "Image",
@ -50,11 +73,10 @@ def store(%{"img" => "data:image/" <> image_data}) do
%{ %{
"type" => "Link", "type" => "Link",
"mediaType" => content_type, "mediaType" => content_type,
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(filename))) "href" => url_path
} }
], ],
"name" => filename, "name" => name
"uuid" => uuid
} }
end end
@ -63,6 +85,65 @@ def upload_path do
Keyword.fetch!(settings, :uploads) Keyword.fetch!(settings, :uploads)
end end
defp create_name(uuid, ext, type) do
case type do
"application/octet-stream" ->
String.downcase(Enum.join([uuid, ext], "."))
"audio/mpeg" ->
String.downcase(Enum.join([uuid, "mp3"], "."))
_ ->
String.downcase(Enum.join([uuid, List.last(String.split(type, "/"))], "."))
end
end
defp get_uuid(file, should_dedupe) do
if should_dedupe do
Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
else
UUID.generate()
end
end
defp get_name(file, uuid, type, should_dedupe) do
if should_dedupe do
create_name(uuid, List.last(String.split(file.filename, ".")), type)
else
unless String.contains?(file.filename, ".") do
case type do
"image/png" -> file.filename <> ".png"
"image/jpeg" -> file.filename <> ".jpg"
"image/gif" -> file.filename <> ".gif"
"video/webm" -> file.filename <> ".webm"
"video/mp4" -> file.filename <> ".mp4"
"audio/mpeg" -> file.filename <> ".mp3"
"audio/ogg" -> file.filename <> ".ogg"
"audio/wav" -> file.filename <> ".wav"
_ -> file.filename
end
else
file.filename
end
end
end
defp get_upload_path(uuid, should_dedupe) do
if should_dedupe do
upload_path()
else
Path.join(upload_path(), uuid)
end
end
defp get_url(name, uuid, should_dedupe) do
if should_dedupe do
url_for(:cow_uri.urlencode(name))
else
url_for(Path.join(uuid, :cow_uri.urlencode(name)))
end
end
defp url_for(file) do defp url_for(file) do
"#{Web.base_url()}/media/#{file}" "#{Web.base_url()}/media/#{file}"
end end
@ -89,6 +170,9 @@ def get_content_type(file) do
<<0x49, 0x44, 0x33, _, _, _, _, _>> -> <<0x49, 0x44, 0x33, _, _, _, _, _>> ->
"audio/mpeg" "audio/mpeg"
<<255, 251, _, 68, 0, 0, 0, 0>> ->
"audio/mpeg"
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> -> <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
"audio/ogg" "audio/ogg"

View file

@ -201,6 +201,14 @@ def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
end end
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
@user_config Application.get_env(:pleroma, :user) @user_config Application.get_env(:pleroma, :user)
@deny_follow_blocked Keyword.get(@user_config, :deny_follow_blocked) @deny_follow_blocked Keyword.get(@user_config, :deny_follow_blocked)
@ -259,6 +267,10 @@ def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address) Enum.member?(follower.following, followed.follower_address)
end end
def locked?(%User{} = user) do
user.info["locked"] || false
end
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id) Repo.get_by(User, ap_id: ap_id)
end end
@ -356,6 +368,40 @@ def get_friends(user) do
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
end end
def get_follow_requests_query(%User{} = user) do
from(
a in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
a.data
),
where:
fragment(
"? ->> 'state' = 'pending'",
a.data
),
where:
fragment(
"? @> ?",
a.data,
^%{"object" => user.ap_id}
)
)
end
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
users =
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
{:ok, users}
end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
note_count = (user.info["note_count"] || 0) + 1 note_count = (user.info["note_count"] || 0) + 1
new_info = Map.put(user.info, "note_count", note_count) new_info = Map.put(user.info, "note_count", note_count)
@ -486,7 +532,31 @@ def unblock(user, %{ap_id: ap_id}) do
def blocks?(user, %{ap_id: ap_id}) do def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || [] blocks = user.info["blocks"] || []
Enum.member?(blocks, ap_id) domain_blocks = user.info["domain_blocks"] || []
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
Enum.any?(domain_blocks, fn domain ->
host == domain
end)
end
def block_domain(user, domain) do
domain_blocks = user.info["domain_blocks"] || []
new_blocks = Enum.uniq([domain | domain_blocks])
new_info = Map.put(user.info, "domain_blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def unblock_domain(user, domain) do
blocks = user.info["domain_blocks"] || []
new_blocks = List.delete(blocks, domain)
new_info = Map.put(user.info, "domain_blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end end
def local_user_query() do def local_user_query() do

View file

@ -57,6 +57,7 @@ def stream_out(activity) do
if activity.data["type"] in ["Create", "Announce"] do if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity) Pleroma.Web.Streamer.stream("public", activity)
@ -198,7 +199,7 @@ def unannounce(
:ok <- maybe_federate(unannounce_activity), :ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity), {:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do {:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, announce_activity, object} {:ok, unannounce_activity, object}
else else
_e -> {:ok, object} _e -> {:ok, object}
end end
@ -214,6 +215,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -449,11 +451,13 @@ defp restrict_recent(query, _) do
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || [] blocks = info["blocks"] || []
domain_blocks = info["domain_blocks"] || []
from( from(
activity in query, activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks), where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks) where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
) )
end end
@ -502,7 +506,7 @@ def fetch_activities(recipients, opts \\ %{}) do
end end
def upload(file) do def upload(file) do
data = Upload.store(file) data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media])
Repo.insert(%Object{data: data}) Repo.insert(%Object{data: data})
end end

View file

@ -2,6 +2,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
alias Pleroma.User alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF @behaviour Pleroma.Web.ActivityPub.MRF
@mrf_rejectnonpublic Application.get_env(:pleroma, :mrf_rejectnonpublic)
@allow_followersonly Keyword.get(@mrf_rejectnonpublic, :allow_followersonly)
@allow_direct Keyword.get(@mrf_rejectnonpublic, :allow_direct)
@impl true @impl true
def filter(object) do def filter(object) do
if object["type"] == "Create" do if object["type"] == "Create" do
@ -18,9 +22,25 @@ def filter(object) do
end end
case visibility do case visibility do
"public" -> {:ok, object} "public" ->
"unlisted" -> {:ok, object} {:ok, object}
_ -> {:reject, nil}
"unlisted" ->
{:ok, object}
"followers" ->
with true <- @allow_followersonly do
{:ok, object}
else
_e -> {:reject, nil}
end
"direct" ->
with true <- @allow_direct do
{:ok, object}
else
_e -> {:reject, nil}
end
end end
else else
{:ok, object} {:ok, object}

View file

@ -30,14 +30,19 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do case ActivityPub.fetch_object_from_id(in_reply_to_id) do
{:ok, replied_object} -> {:ok, replied_object} ->
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) with %Activity{} = activity <-
Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
object object
|> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id) |> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"]) |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"]) |> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
object
end
e -> e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
@ -137,9 +142,17 @@ def handle_incoming(
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower), %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true}) if not User.locked?(followed) do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: data,
local: true
})
User.follow(follower, followed) User.follow(follower, followed)
end
{:ok, activity} {:ok, activity}
else else
_e -> :error _e -> :error
@ -252,7 +265,7 @@ def handle_incoming(
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"] banner = new_user_data[:info]["banner"]
locked = new_user_data[:info]["locked"] locked = new_user_data[:info]["locked"] || false
update_data = update_data =
new_user_data new_user_data
@ -304,7 +317,7 @@ def handle_incoming(
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- {:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity} {:ok, activity}
else else
_e -> :error _e -> :error
@ -432,6 +445,58 @@ def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = obj
{:ok, data} {:ok, data}
end end
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do
follow_activity_id =
if is_binary(data["object"]) do
data["object"]
else
data["object"]["id"]
end
with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
end
def prepare_outgoing(%{"type" => "Reject"} = data) do
follow_activity_id =
if is_binary(data["object"]) do
data["object"]
else
data["object"]["id"]
end
with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
end
def prepare_outgoing(%{"type" => _type} = data) do def prepare_outgoing(%{"type" => _type} = data) do
data = data =
data data

View file

@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID} alias Ecto.{Changeset, UUID}
import Ecto.Query import Ecto.Query
require Logger
# Some implementations send the actor URI as the actor field, others send the entire actor object, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
@ -216,10 +217,27 @@ def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
#### Follow-related helpers #### Follow-related helpers
@doc """
Updates a follow activity's state (for locked accounts).
"""
def update_follow_state(%Activity{} = activity, state) do
with new_data <-
activity.data
|> Map.put("state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
@doc """ @doc """
Makes a follow activity data for the given follower and followed Makes a follow activity data for the given follower and followed
""" """
def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activity_id) do def make_follow_data(
%User{ap_id: follower_id},
%User{ap_id: followed_id} = followed,
activity_id
) do
data = %{ data = %{
"type" => "Follow", "type" => "Follow",
"actor" => follower_id, "actor" => follower_id,
@ -228,7 +246,10 @@ def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activ
"object" => followed_id "object" => followed_id
} }
if activity_id, do: Map.put(data, "id", activity_id), else: data data = if activity_id, do: Map.put(data, "id", activity_id), else: data
data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data
data
end end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do

View file

@ -4,6 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.{CommonAPI, OStatus} alias Pleroma.Web.{CommonAPI, OStatus}
alias Pleroma.Web.OAuth.{Authorization, Token, App} alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
@ -71,6 +72,20 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
user user
end end
user =
if locked = params["locked"] do
with locked <- locked == "true",
new_info <- Map.put(user.info, "locked", locked),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do if original_user != user do
@ -345,7 +360,7 @@ def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
end end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}) render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end end
@ -476,6 +491,53 @@ def following(conn, %{"id" => id}) do
end end
end end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
end
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, follower} <- User.maybe_direct_follow(follower, followed),
@ -545,6 +607,20 @@ def blocks(%{assigns: %{user: user}} = conn, _) do
end end
end end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info["domain_blocks"] || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true") accounts = User.search(query, params["resolve"] == "true")

View file

@ -15,10 +15,13 @@ def connect(params, socket) do
with token when not is_nil(token) <- params["access_token"], with token when not is_nil(token) <- params["access_token"],
%Token{user_id: user_id} <- Repo.get_by(Token, token: token), %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id), %User{} = user <- Repo.get(User, user_id),
stream when stream in ["public", "public:local", "user", "direct"] <- params["stream"] do stream when stream in ["public", "public:local", "user", "direct", "list"] <-
params["stream"] do
topic = if stream == "list", do: "list:#{params["list"]}", else: stream
socket = socket =
socket socket
|> assign(:topic, params["stream"]) |> assign(:topic, topic)
|> assign(:user, user) |> assign(:user, user)
Pleroma.Web.Streamer.add_socket(params["stream"], socket) Pleroma.Web.Streamer.add_socket(params["stream"], socket)

View file

@ -125,8 +125,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
uri: object["id"], uri: object["id"],
url: object["external_url"] || object["id"], url: object["external_url"] || object["id"],
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: reply_to && reply_to.id, in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && reply_to_user.id, in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
content: HtmlSanitizeEx.basic_html(object["content"]), content: HtmlSanitizeEx.basic_html(object["content"]),
created_at: created_at, created_at: created_at,

View file

@ -81,10 +81,10 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
# - investigate a way to verify the user wants to grant read/write/follow once scope handling is done # - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
def token_exchange( def token_exchange(
conn, conn,
%{"grant_type" => "password", "name" => name, "password" => password} = params %{"grant_type" => "password", "username" => name, "password" => password} = params
) do ) do
with %App{} = app <- get_app_from_request(conn, params), with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_cached_by_nickname(name), %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, auth} <- Authorization.create_authorization(app, user), {:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
@ -104,6 +104,18 @@ def token_exchange(
end end
end end
def token_exchange(
conn,
%{"grant_type" => "password", "name" => name, "password" => password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
defp fix_padding(token) do defp fix_padding(token) do
token token
|> Base.url_decode64!(padding: false) |> Base.url_decode64!(padding: false)

View file

@ -41,7 +41,7 @@ def user_fetcher(username) do
end end
pipeline :well_known do pipeline :well_known do
plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"]) plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end end
pipeline :config do pipeline :config do
@ -97,12 +97,14 @@ def user_fetcher(username) do
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop) post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop) post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
get("/follow_requests", MastodonAPIController, :follow_requests)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follows", MastodonAPIController, :follow) post("/follows", MastodonAPIController, :follow)
get("/blocks", MastodonAPIController, :blocks) get("/blocks", MastodonAPIController, :blocks)
get("/domain_blocks", MastodonAPIController, :empty_array)
get("/follow_requests", MastodonAPIController, :empty_array)
get("/mutes", MastodonAPIController, :empty_array) get("/mutes", MastodonAPIController, :empty_array)
get("/timelines/home", MastodonAPIController, :home_timeline) get("/timelines/home", MastodonAPIController, :home_timeline)
@ -134,6 +136,10 @@ def user_fetcher(username) do
get("/lists/:id/accounts", MastodonAPIController, :list_accounts) get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list) post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list) delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
end end
scope "/api/web", Pleroma.Web.MastodonAPI do scope "/api/web", Pleroma.Web.MastodonAPI do
@ -238,8 +244,13 @@ def user_fetcher(username) do
post("/statuses/update", TwitterAPI.Controller, :status_update) post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet) post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post) post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
post("/friendships/create", TwitterAPI.Controller, :follow) post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow) post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block) post("/blocks/create", TwitterAPI.Controller, :block)

View file

@ -1,7 +1,7 @@
defmodule Pleroma.Web.Streamer do defmodule Pleroma.Web.Streamer do
use GenServer use GenServer
require Logger require Logger
alias Pleroma.{User, Notification} alias Pleroma.{User, Notification, Activity, Object}
def init(args) do def init(args) do
{:ok, args} {:ok, args}
@ -59,6 +59,19 @@ def handle_cast(%{action: :stream, topic: "direct", item: item}, topics) do
{:noreply, topics} {:noreply, topics}
end end
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
recipient_topics =
Pleroma.List.get_lists_from_activity(item)
|> Enum.map(fn %{id: id} -> "list:#{id}" end)
Enum.each(recipient_topics || [], fn list_topic ->
Logger.debug("Trying to push message to #{list_topic}\n\n")
push_to_socket(topics, list_topic, item)
end)
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do
topic = "user:#{item.user_id}" topic = "user:#{item.user_id}"
@ -125,6 +138,34 @@ def handle_cast(m, state) do
{:noreply, state} {:noreply, state}
end end
defp represent_update(%Activity{} = activity, %User{} = user) do
%{
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
activity: activity,
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.
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
blocks = user.info["blocks"] || []
parent = Object.get_by_ap_id(item.data["object"])
unless is_nil(parent) or item.actor in blocks or parent.data["actor"] in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
end
end)
end
def push_to_socket(topics, topic, item) do def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn socket -> Enum.each(topics[topic] || [], fn socket ->
# Get the current user so we have up-to-date blocks etc. # Get the current user so we have up-to-date blocks etc.
@ -132,20 +173,7 @@ def push_to_socket(topics, topic, item) do
blocks = user.info["blocks"] || [] blocks = user.info["blocks"] || []
unless item.actor in blocks do unless item.actor in blocks do
json = send(socket.transport_pid, {:text, represent_update(item, user)})
%{
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
activity: item,
for: user
)
|> Jason.encode!()
}
|> Jason.encode!()
send(socket.transport_pid, {:text, json})
end end
end) end)
end end

View file

@ -12,14 +12,9 @@ def create_status(%User{} = user, %{"status" => _} = data) do
end end
def delete(%User{} = user, id) do def delete(%User{} = user, id) do
# TwitterAPI does not have an "unretweet" endpoint; instead this is done with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id),
# via the "destroy" endpoint. Therefore, we need to handle {:ok, activity} <- CommonAPI.delete(id, user) do
# when the status to "delete" is actually an Announce (repeat) object. {:ok, activity}
with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id) do
case type do
"Announce" -> unrepeat(user, id)
_ -> CommonAPI.delete(id, user)
end
end end
end end
@ -70,8 +65,9 @@ def repeat(%User{} = user, ap_id_or_id) do
end end
end end
defp unrepeat(%User{} = user, ap_id_or_id) do def unrepeat(%User{} = user, ap_id_or_id) do
with {:ok, _unannounce, activity, _object} <- CommonAPI.unrepeat(ap_id_or_id, user) do with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity} {:ok, activity}
end end
end end

View file

@ -4,6 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.{Repo, Activity, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset alias Ecto.Changeset
require Logger require Logger
@ -240,6 +241,13 @@ def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
end end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unrepeat(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def register(conn, params) do def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do with {:ok, user} <- TwitterAPI.register_user(params) do
render(conn, UserView, "show.json", %{user: user}) render(conn, UserView, "show.json", %{user: user})
@ -331,6 +339,54 @@ def friends(conn, params) do
end end
end end
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
render(conn, UserView, "index.json", %{users: friend_requests, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friend requests")
end
end
def approve_friend_request(conn, %{"user_id" => uid} = params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
end
end
def deny_friend_request(conn, %{"user_id" => uid} = params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
end
end
def friends_ids(%{assigns: %{user: user}} = conn, _params) do def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do with {:ok, friends} <- User.get_friends(user) do
ids = ids =
@ -357,6 +413,20 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
params params
end end
user =
if locked = params["locked"] do
with locked <- locked == "true",
new_info <- Map.put(user.info, "locked", locked),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user) CommonAPI.update(user)

View file

@ -51,7 +51,8 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(), "cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(), "background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
"is_local" => user.local "is_local" => user.local,
"locked" => !!user.info["locked"]
} }
if assigns[:token] do if assigns[:token] do

View file

@ -25,35 +25,17 @@ def host_meta do
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
def webfinger(resource, "JSON") do def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host() host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/ regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource) do with %{"username" => username} <- Regex.named_captures(regex, resource),
user = User.get_by_nickname(username) %User{} = user <- User.get_by_nickname(username) do
{:ok, represent_user(user, "JSON")} {:ok, represent_user(user, fmt)}
else else
_e -> _e ->
with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do with %User{} = user <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, "JSON")} {:ok, represent_user(user, fmt)}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
def webfinger(resource, "XML") do
host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource) do
user = User.get_by_nickname(username)
{:ok, represent_user(user, "XML")}
else
_e ->
with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, "XML")}
else else
_e -> _e ->
{:error, "Couldn't find user"} {:error, "Couldn't find user"}

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddListFollowIndex do
use Ecto.Migration
def change do
create index(:lists, [:following])
end
end

View file

@ -0,0 +1,8 @@
defmodule Pleroma.Repo.Migrations.CreateApidHostExtractionIndex do
use Ecto.Migration
@disable_ddl_transaction true
def change do
create index(:activities, ["(split_part(actor, '/', 3))"], concurrently: true, name: :activities_hosts)
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 997 B

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.c0e1e1e1fcff94fd1e14fc44bfee9a1e.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.16ab7851cdbf730f9cbc.js></script><script type=text/javascript src=/static/js/vendor.56aa9f8c34786f6af6b7.js></script><script type=text/javascript src=/static/js/app.13c0bda10eb515cdf8ed.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.5d0189b6f119febde070b703869bbd06.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.f2341edd686e54ee9b4a.js></script><script type=text/javascript src=/static/js/vendor.a93310d51acbd9480094.js></script><script type=text/javascript src=/static/js/app.de965bb2a0a8bffbeafa.js></script></body></html>

View file

@ -10,5 +10,6 @@
"whoToFollowProviderDummy2": "https://followlink.osa-p.net/api/get_recommend.json?acct=@{{user}}@{{host}}", "whoToFollowProviderDummy2": "https://followlink.osa-p.net/api/get_recommend.json?acct=@{{user}}@{{host}}",
"whoToFollowLink": "https://vinayaka.distsn.org/?{{host}}+{{user}}", "whoToFollowLink": "https://vinayaka.distsn.org/?{{host}}+{{user}}",
"whoToFollowLinkDummy2": "https://followlink.osa-p.net/recommend.html", "whoToFollowLinkDummy2": "https://followlink.osa-p.net/recommend.html",
"showInstanceSpecificPanel": false "showInstanceSpecificPanel": false,
"scopeOptionsEnabled": false
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
!function(e){function t(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var p,l,s=0,i=[];s<c.length;s++)l=c[s],n[l]&&i.push.apply(i,n[l]),n[l]=0;for(p in o)Object.prototype.hasOwnProperty.call(o,p)&&(e[p]=o[p]);for(a&&a(c,o);i.length;)i.shift().call(null,t);if(o[0])return r[0]=0,t(0)};var r={},n={0:0};t.e=function(e,a){if(0===n[e])return a.call(null,t);if(void 0!==n[e])n[e].push(a);else{n[e]=[a];var r=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"56aa9f8c34786f6af6b7",2:"13c0bda10eb515cdf8ed"}[e]+".js",r.appendChild(c)}},t.m=e,t.c=r,t.p="/"}([]);
//# sourceMappingURL=manifest.16ab7851cdbf730f9cbc.js.map

View file

@ -0,0 +1,2 @@
!function(e){function t(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var a=window.webpackJsonp;window.webpackJsonp=function(o,p){for(var c,l,s=0,i=[];s<o.length;s++)l=o[s],n[l]&&i.push.apply(i,n[l]),n[l]=0;for(c in p)Object.prototype.hasOwnProperty.call(p,c)&&(e[c]=p[c]);for(a&&a(o,p);i.length;)i.shift().call(null,t);if(p[0])return r[0]=0,t(0)};var r={},n={0:0};t.e=function(e,a){if(0===n[e])return a.call(null,t);if(void 0!==n[e])n[e].push(a);else{n[e]=[a];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"a93310d51acbd9480094",2:"de965bb2a0a8bffbeafa"}[e]+".js",r.appendChild(o)}},t.m=e,t.c=r,t.p="/"}([]);
//# sourceMappingURL=manifest.f2341edd686e54ee9b4a.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -74,4 +74,20 @@ test "getting all lists by an user" do
assert list_two in lists assert list_two in lists
refute list_three in lists refute list_three in lists
end end
test "getting all lists the user is a member of" do
user = insert(:user)
other_user = insert(:user)
{:ok, list_one} = Pleroma.List.create("title", user)
{:ok, list_two} = Pleroma.List.create("other title", user)
{:ok, list_three} = Pleroma.List.create("third title", other_user)
{:ok, list_one} = Pleroma.List.follow(list_one, other_user)
{:ok, list_two} = Pleroma.List.follow(list_two, other_user)
{:ok, list_three} = Pleroma.List.follow(list_three, user)
lists = Pleroma.List.get_lists_from_activity(%Pleroma.Activity{actor: other_user.ap_id})
assert list_one in lists
assert list_two in lists
refute list_three in lists
end
end end

View file

@ -3,40 +3,58 @@ defmodule Pleroma.UploadTest do
use Pleroma.DataCase use Pleroma.DataCase
describe "Storing a file" do describe "Storing a file" do
test "copies the file to the configured folder" do test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{ file = %Plug.Upload{
content_type: "image/jpg", content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"), path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg" filename: "an [image.jpg"
} }
data = Upload.store(file) data = Upload.store(file, true)
assert data["name"] == "an [image.jpg"
assert List.first(data["url"])["href"] == assert data["name"] ==
"http://localhost:4001/media/#{data["uuid"]}/an%20%5Bimage.jpg" "e7a6d0cf595bff76f14c9a98b6c199539559e8b844e02e51e5efcfd1f614a2df.jpeg"
end end
test "fixes an incorrect content type" do test "copies the file to the configured folder without deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{ file = %Plug.Upload{
content_type: "application/octet-stream", content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"), path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg" filename: "an [image.jpg"
} }
data = Upload.store(file) data = Upload.store(file, false)
assert data["name"] == "an [image.jpg"
end
test "fixes incorrect content type" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "application/octet-stream",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
data = Upload.store(file, true)
assert hd(data["url"])["mediaType"] == "image/jpeg" assert hd(data["url"])["mediaType"] == "image/jpeg"
end end
test "does not modify a valid content type" do test "adds missing extension" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{ file = %Plug.Upload{
content_type: "image/png", content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"), path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg" filename: "an [image"
} }
data = Upload.store(file) data = Upload.store(file, false)
assert hd(data["url"])["mediaType"] == "image/png" assert data["name"] == "an [image.jpg"
end end
end end
end end

View file

@ -361,6 +361,27 @@ test "it unblocks users" do
end end
end end
describe "domain blocking" do
test "blocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
assert User.blocks?(user, collateral_user)
end
test "unblocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
{:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
end
test "get recipients from activity" do test "get recipients from activity" do
actor = insert(:user) actor = insert(:user)
user = insert(:user, local: true) user = insert(:user, local: true)

View file

@ -318,11 +318,9 @@ test "unannouncing a previously announced object" do
{:ok, announce_activity, object} = ActivityPub.announce(user, object) {:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1 assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, activity, object} = ActivityPub.unannounce(user, object) {:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0 assert object.data["announcement_count"] == 0
assert activity == announce_activity
assert unannounce_activity.data["to"] == [ assert unannounce_activity.data["to"] == [
User.ap_followers(user), User.ap_followers(user),
announce_activity.data["actor"] announce_activity.data["actor"]

View file

@ -4,6 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.{Repo, User, Activity, Notification} alias Pleroma.{Repo, User, Activity, Notification}
alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory import Pleroma.Factory
import ExUnit.CaptureLog import ExUnit.CaptureLog
@ -644,6 +645,73 @@ test "returns the relationships for the current user", %{conn: conn} do
end end
end end
describe "locked accounts" do
test "/api/v1/follow_requests works" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/follow_requests")
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
test "/api/v1/follow_requests/:id/authorize works" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/authorize")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == true
end
test "/api/v1/follow_requests/:id/reject works" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/reject")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
end
end
test "account fetching", %{conn: conn} do test "account fetching", %{conn: conn} do
user = insert(:user) user = insert(:user)
@ -792,6 +860,46 @@ test "getting a list of blocks", %{conn: conn} do
assert [%{"id" => ^other_user_id}] = json_response(conn, 200) assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end end
test "blocking / unblocking a domain", %{conn: conn} do
user = insert(:user)
other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)
conn =
build_conn()
|> assign(:user, user)
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user)
end
test "getting a list of domain blocks" do
user = insert(:user)
{:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")
domain_blocks = json_response(conn, 200)
assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
end
test "unimplemented mute endpoints" do test "unimplemented mute endpoints" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -64,11 +64,11 @@ test "a reply" do
status = StatusView.render("status.json", %{activity: activity}) status = StatusView.render("status.json", %{activity: activity})
assert status.in_reply_to_id == note.id assert status.in_reply_to_id == to_string(note.id)
[status] = StatusView.render("index.json", %{activities: [activity], as: :activity}) [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
assert status.in_reply_to_id == note.id assert status.in_reply_to_id == to_string(note.id)
end end
test "contains mentions" do test "contains mentions" do

View file

@ -580,6 +580,40 @@ test "with credentials", %{conn: conn, user: current_user} do
end end
end end
describe "POST /api/statuses/unretweet/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/statuses/unretweet/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
request_path = "/api/statuses/retweet/#{note_activity.id}.json"
_response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
request_path = String.replace(request_path, "retweet", "unretweet")
response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
activity = Repo.get(Activity, note_activity.id)
activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"])
assert json_response(response, 200) ==
ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user})
end
end
describe "POST /api/account/register" do describe "POST /api/account/register" do
test "it creates a new user", %{conn: conn} do test "it creates a new user", %{conn: conn} do
data = %{ data = %{
@ -762,6 +796,38 @@ test "it updates a user's profile", %{conn: conn} do
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end end
test "it locks an account", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"locked" => "true"
})
user = Repo.get!(User, user.id)
assert user.info["locked"] == true
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
test "it unlocks an account", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"locked" => "false"
})
user = Repo.get!(User, user.id)
assert user.info["locked"] == false
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
end end
defp valid_user(_context) do defp valid_user(_context) do
@ -926,4 +992,72 @@ test "with credentials and valid password", %{conn: conn, user: current_user} do
:timer.sleep(1000) :timer.sleep(1000)
end end
end end
describe "GET /api/pleroma/friend_requests" do
test "it lists friend requests" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/friend_requests")
assert [relationship] = json_response(conn, 200)
assert other_user.id == relationship["id"]
end
end
describe "POST /api/pleroma/friendships/approve" do
test "it approves a friend request" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)})
assert relationship = json_response(conn, 200)
assert other_user.id == relationship["id"]
assert relationship["follows_you"] == true
end
end
describe "POST /api/pleroma/friendships/deny" do
test "it denies a friend request" do
user = insert(:user, %{info: %{"locked" => true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)})
assert relationship = json_response(conn, 200)
assert other_user.id == relationship["id"]
assert relationship["follows_you"] == false
end
end
end end

View file

@ -228,6 +228,17 @@ test "it retweets a status and returns the retweet" do
assert status == updated_activity assert status == updated_activity
end end
test "it unretweets an already retweeted status" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, _status} = TwitterAPI.repeat(user, note_activity.id)
{:ok, status} = TwitterAPI.unrepeat(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it registers a new user and returns the user." do test "it registers a new user and returns the user." do
data = %{ data = %{
"nickname" => "lain", "nickname" => "lain",

View file

@ -59,7 +59,8 @@ test "A user" do
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => banner, "cover_photo" => banner,
"background_image" => nil, "background_image" => nil,
"is_local" => true "is_local" => true,
"locked" => false
} }
assert represented == UserView.render("show.json", %{user: user}) assert represented == UserView.render("show.json", %{user: user})
@ -94,7 +95,8 @@ test "A user for a given other follower", %{user: user} do
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => banner, "cover_photo" => banner,
"background_image" => nil, "background_image" => nil,
"is_local" => true "is_local" => true,
"locked" => false
} }
assert represented == UserView.render("show.json", %{user: user, for: follower}) assert represented == UserView.render("show.json", %{user: user, for: follower})
@ -130,7 +132,8 @@ test "A user that follows you", %{user: user} do
"statusnet_profile_url" => follower.ap_id, "statusnet_profile_url" => follower.ap_id,
"cover_photo" => banner, "cover_photo" => banner,
"background_image" => nil, "background_image" => nil,
"is_local" => true "is_local" => true,
"locked" => false
} }
assert represented == UserView.render("show.json", %{user: follower, for: user}) assert represented == UserView.render("show.json", %{user: follower, for: user})
@ -173,7 +176,8 @@ test "A blocked user for the blocker" do
"statusnet_profile_url" => user.ap_id, "statusnet_profile_url" => user.ap_id,
"cover_photo" => banner, "cover_photo" => banner,
"background_image" => nil, "background_image" => nil,
"is_local" => true "is_local" => true,
"locked" => false
} }
blocker = Repo.get(User, blocker.id) blocker = Repo.get(User, blocker.id)