diff options
Diffstat (limited to 'lib')
37 files changed, 614 insertions, 179 deletions
diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex index 4d427cfd8..a454a958e 100644 --- a/lib/mix/tasks/make_moderator.ex +++ b/lib/mix/tasks/make_moderator.ex @@ -5,7 +5,7 @@ defmodule Mix.Tasks.SetModerator do @shortdoc "Set moderator status" def run([nickname | rest]) do - ensure_started(Repo, []) + Application.ensure_all_started(:pleroma) moderator = case rest do diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index dd6805125..bed96861f 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -78,4 +78,8 @@ defmodule Pleroma.Activity do end def get_create_activity_by_object_ap_id(_), do: nil + + def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"]) + def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id) + def normalize(_), do: nil end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index df7ffbc41..d199c9243 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -116,7 +116,28 @@ defmodule Pleroma.Formatter do _ -> [] end) - @emoji @finmoji_with_filenames ++ @emoji_from_file + @emoji_from_globs ( + static_path = Path.join(:code.priv_dir(:pleroma), "static") + + globs = + Application.get_env(:pleroma, :emoji, []) + |> Keyword.get(:shortcode_globs, []) + + paths = + Enum.map(globs, fn glob -> + Path.join(static_path, glob) + |> Path.wildcard() + end) + |> Enum.concat() + + Enum.map(paths, fn path -> + shortcode = Path.basename(path, Path.extname(path)) + external_path = Path.join("/", Path.relative_to(path, static_path)) + {shortcode, external_path} + end) + ) + + @emoji @finmoji_with_filenames ++ @emoji_from_globs ++ @emoji_from_file def emojify(text, emoji \\ @emoji) def emojify(text, nil), do: text @@ -223,8 +244,8 @@ defmodule Pleroma.Formatter do subs = subs ++ - Enum.map(tags, fn {_, tag, uuid} -> - url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>" + Enum.map(tags, fn {tag_text, tag, uuid} -> + url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>" {uuid, url} end) diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index f6abcd4d0..97a1dea77 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -54,7 +54,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do String.split(text, "\r") |> Enum.map(fn text -> - "i#{text}\tfake\(NULL)\t0\r\n" + "i#{text}\tfake\t(NULL)\t0\r\n" end) |> Enum.join("") end @@ -77,14 +77,14 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> info("#{like_count} likes, #{announcement_count} repeats") <> - "\r\n" <> + "i\tfake\t(NULL)\t0\r\n" <> info( HtmlSanitizeEx.strip_tags( String.replace(activity.data["object"]["content"], "<br>", "\r") ) ) end) - |> Enum.join("\r\n") + |> Enum.join("i\tfake\t(NULL)\t0\r\n") end def response("") do diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index ff2af4a6f..1bcff5a7b 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -27,6 +27,10 @@ defmodule Pleroma.Object do Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id))) end + def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"]) + def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id) + def normalize(_), do: nil + def get_cached_by_ap_id(ap_id) do if Mix.env() == :test do get_by_ap_id(ap_id) diff --git a/lib/pleroma/plugs/digest.ex b/lib/pleroma/plugs/digest.ex new file mode 100644 index 000000000..9d6bbb085 --- /dev/null +++ b/lib/pleroma/plugs/digest.ex @@ -0,0 +1,10 @@ +defmodule Pleroma.Web.Plugs.DigestPlug do + alias Plug.Conn + require Logger + + def read_body(conn, opts) do + {:ok, body, conn} = Conn.read_body(conn, opts) + digest = "SHA-256=" <> (:crypto.hash(:sha256, body) |> Base.encode64()) + {:ok, body, Conn.assign(conn, :digest, digest)} + end +end diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex index 38bcd3a78..9e53371b7 100644 --- a/lib/pleroma/plugs/http_signature.ex +++ b/lib/pleroma/plugs/http_signature.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do cond do signature && String.contains?(signature, user) -> + # set (request-target) header to the appropriate value + # we also replace the digest header with the one we computed conn = conn |> put_req_header( @@ -26,6 +28,14 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do String.downcase("#{conn.method}") <> " #{conn.request_path}" ) + conn = + if conn.assigns[:digest] do + conn + |> put_req_header("digest", conn.assigns[:digest]) + else + conn + end + assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn)) signature -> diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index ab4bd16f0..408a3fc56 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -18,8 +18,10 @@ defmodule Pleroma.Upload do File.cp!(file.path, result_file) end + strip_exif_data(content_type, result_file) + %{ - "type" => "Image", + "type" => "Document", "url" => [ %{ "type" => "Link", @@ -67,6 +69,8 @@ defmodule Pleroma.Upload do File.rename(uuidpath, result_file) end + strip_exif_data(content_type, result_file) + %{ "type" => "Image", "url" => [ @@ -80,16 +84,31 @@ defmodule Pleroma.Upload do } end + def strip_exif_data(content_type, file) do + settings = Application.get_env(:pleroma, Pleroma.Upload) + do_strip = Keyword.fetch!(settings, :strip_exif) + [filetype, ext] = String.split(content_type, "/") + + if filetype == "image" and do_strip == true do + Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true) + end + end + def upload_path do settings = Application.get_env(:pleroma, Pleroma.Upload) Keyword.fetch!(settings, :uploads) end defp create_name(uuid, ext, type) do - if type == "application/octet-stream" do - String.downcase(Enum.join([uuid, ext], ".")) - else - String.downcase(Enum.join([uuid, List.last(String.split(type, "/"))], ".")) + 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 @@ -105,7 +124,21 @@ defmodule Pleroma.Upload do if should_dedupe do create_name(uuid, List.last(String.split(file.filename, ".")), type) else - file.filename + 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 @@ -151,6 +184,9 @@ defmodule Pleroma.Upload do <<0x49, 0x44, 0x33, _, _, _, _, _>> -> "audio/mpeg" + <<255, 251, _, 68, 0, 0, 0, 0>> -> + "audio/mpeg" + <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> -> "audio/ogg" diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index b27397e13..fa0ea171d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -169,6 +169,9 @@ defmodule Pleroma.User do end def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do + user_config = Application.get_env(:pleroma, :user) + deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked) + user_info = user_info(followed) should_direct_follow = @@ -178,7 +181,8 @@ defmodule Pleroma.User do false # if the users are blocking each other, we shouldn't even be here, but check for it anyway - User.blocks?(follower, followed) == true or User.blocks?(followed, follower) == true -> + deny_follow_blocked and + (User.blocks?(follower, followed) or User.blocks?(followed, follower)) -> false # if OStatus, then there is no three-way handshake to follow @@ -206,13 +210,16 @@ defmodule Pleroma.User do end def follow(%User{} = follower, %User{info: info} = followed) do + user_config = Application.get_env(:pleroma, :user) + deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked) + ap_followers = followed.follower_address cond do following?(follower, followed) or info["deactivated"] -> {:error, "Could not follow user: #{followed.nickname} is already on your list."} - blocks?(followed, follower) -> + deny_follow_blocked and blocks?(followed, follower) -> {:error, "Could not follow user: #{followed.nickname} blocked you."} true -> @@ -391,6 +398,7 @@ defmodule Pleroma.User do Enum.map(reqs, fn req -> req.actor end) |> Enum.uniq() |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) + |> Enum.filter(fn u -> !following?(u, user) end) {:ok, users} end @@ -505,15 +513,33 @@ defmodule Pleroma.User do Repo.all(q) end - def block(user, %{ap_id: ap_id}) do - blocks = user.info["blocks"] || [] + def block(blocker, %User{ap_id: ap_id} = blocked) do + # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213) + blocker = + if following?(blocker, blocked) do + {:ok, blocker, _} = unfollow(blocker, blocked) + blocker + else + blocker + end + + if following?(blocked, blocker) do + unfollow(blocked, blocker) + end + + blocks = blocker.info["blocks"] || [] new_blocks = Enum.uniq([ap_id | blocks]) - new_info = Map.put(user.info, "blocks", new_blocks) + new_info = Map.put(blocker.info, "blocks", new_blocks) - cs = User.info_changeset(user, %{info: new_info}) + cs = User.info_changeset(blocker, %{info: new_info}) update_and_set_cache(cs) end + # helper to handle the block given only an actor's AP id + def block(blocker, %{ap_id: ap_id}) do + block(blocker, User.get_by_ap_id(ap_id)) + end + def unblock(user, %{ap_id: ap_id}) do blocks = user.info["blocks"] || [] new_blocks = List.delete(blocks, ap_id) @@ -582,7 +608,7 @@ defmodule Pleroma.User do |> Enum.each(fn activity -> case activity.data["type"] do "Create" -> - ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"])) + ActivityPub.delete(Object.normalize(activity.data["object"])) # TODO: Do something with likes, follows, repeats. _ -> diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 08e21069c..ec605b694 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def insert(map, local \\ true) when is_map(map) do - with nil <- Activity.get_by_ap_id(map["id"]), + with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map), :ok <- check_actor_is_active(map["actor"]), {:ok, map} <- MRF.filter(map), @@ -65,6 +65,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do if activity.local do Pleroma.Web.Streamer.stream("public:local", activity) end + + if activity.data["object"]["attachment"] != [] do + Pleroma.Web.Streamer.stream("public:media", activity) + + if activity.local do + Pleroma.Web.Streamer.stream("public:local:media", activity) + end + end else if !Enum.member?(activity.data["cc"] || [], public) && !Enum.member?( @@ -199,7 +207,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do :ok <- maybe_federate(unannounce_activity), {:ok, _activity} <- Repo.delete(announce_activity), {:ok, object} <- remove_announce_from_object(announce_activity, object) do - {:ok, unannounce_activity, announce_activity, object} + {:ok, unannounce_activity, object} else _e -> {:ok, object} end @@ -243,16 +251,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def block(blocker, blocked, activity_id \\ nil, local \\ true) do - follow_activity = fetch_latest_follow(blocker, blocked) + ap_config = Application.get_env(:pleroma, :activitypub) + unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked) + outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks) - if follow_activity do - unfollow(blocker, blocked, nil, local) + with true <- unfollow_blocked do + follow_activity = fetch_latest_follow(blocker, blocked) + + if follow_activity do + unfollow(blocker, blocked, nil, local) + end end - with block_data <- make_block_data(blocker, blocked, activity_id), + with true <- outgoing_blocks, + block_data <- make_block_data(blocker, blocked, activity_id), {:ok, activity} <- insert(block_data, local), :ok <- maybe_federate(activity) do {:ok, activity} + else + _e -> {:ok, nil} end end @@ -430,6 +447,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_media(query, _), do: query + defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do + from( + activity in query, + where: fragment("?->'object'->>'inReplyTo' is null", activity.data) + ) + end + + defp restrict_replies(query, _), do: query + # Only search through last 100_000 activities by default defp restrict_recent(query, %{"whole_db" => true}), do: query @@ -487,6 +513,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_blocked(opts) |> restrict_media(opts) |> restrict_visibility(opts) + |> restrict_replies(opts) end def fetch_activities(recipients, opts \\ %{}) do @@ -614,13 +641,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do Logger.info("Federating #{id} to #{inbox}") host = URI.parse(inbox).host + digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) + signature = - Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)}) + Pleroma.Web.HTTPSignatures.sign(actor, %{ + host: host, + "content-length": byte_size(json), + digest: digest + }) @httpoison.post( inbox, json, - [{"Content-Type", "application/activity+json"}, {"signature", signature}], + [ + {"Content-Type", "application/activity+json"}, + {"signature", signature}, + {"digest", digest} + ], hackney: [pool: :default] ) end @@ -643,7 +680,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do recv_timeout: 20000 ), {:ok, data} <- Jason.decode(body), - nil <- Object.get_by_ap_id(data["id"]), + nil <- Object.normalize(data), params <- %{ "type" => "Create", "to" => data["to"], @@ -652,7 +689,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "object" => data }, {:ok, activity} <- Transmogrifier.handle_incoming(params) do - {:ok, Object.get_by_ap_id(activity.data["object"]["id"])} + {:ok, Object.normalize(activity.data["object"])} else object = %Object{} -> {:ok, object} @@ -661,7 +698,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do Logger.info("Couldn't get object via AP, trying out OStatus fetching...") case OStatus.fetch_activity_from_url(id) do - {:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])} + {:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"])} e -> e end end diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 879cbe6de..b6936fe90 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -2,6 +2,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do alias Pleroma.User @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 def filter(object) do if object["type"] == "Create" do @@ -18,9 +22,25 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do end case visibility do - "public" -> {:ok, object} - "unlisted" -> {:ok, object} - _ -> {:reject, nil} + "public" -> + {:ok, object} + + "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 else {:ok, object} diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 8d770387d..7fecb8a4f 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -4,6 +4,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do @mrf_policy Application.get_env(:pleroma, :mrf_simple) + @accept Keyword.get(@mrf_policy, :accept) + defp check_accept(actor_info, object) do + if length(@accept) > 0 and not (actor_info.host in @accept) do + {:reject, nil} + else + {:ok, object} + end + end + @reject Keyword.get(@mrf_policy, :reject) defp check_reject(actor_info, object) do if actor_info.host in @reject do @@ -74,7 +83,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do def filter(object) do actor_info = URI.parse(object["actor"]) - with {:ok, object} <- check_reject(actor_info, object), + with {:ok, object} <- check_accept(actor_info, object), + {:ok, object} <- check_reject(actor_info, object), {:ok, object} <- check_media_removal(actor_info, object), {:ok, object} <- check_media_nsfw(actor_info, object), {:ok, object} <- check_ftl_removal(actor_info, object) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index e7a3420d2..2ebc526df 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -13,31 +13,55 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do require Logger + def get_actor(%{"actor" => actor}) when is_binary(actor) do + actor + end + + def get_actor(%{"actor" => actor}) when is_list(actor) do + Enum.at(actor, 0) + end + + def get_actor(%{"actor" => actor_list}) do + Enum.find(actor_list, fn %{"type" => type} -> type == "Person" end) + |> Map.get("id") + end + @doc """ Modifies an incoming AP object (mastodon format) to our internal format. """ def fix_object(object) do object - |> Map.put("actor", object["attributedTo"]) + |> fix_actor |> fix_attachments |> fix_context |> fix_in_reply_to |> fix_emoji |> fix_tag + |> fix_content_map + end + + def fix_actor(%{"attributedTo" => actor} = object) do + object + |> Map.put("actor", get_actor(%{"actor" => actor})) end def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do case ActivityPub.fetch_object_from_id(in_reply_to_id) do {:ok, replied_object} -> - activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) - - object - |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("inReplyToStatusId", activity.id) - |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) - |> Map.put("context", replied_object.data["context"] || object["conversation"]) + with %Activity{} = activity <- + Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do + object + |> Map.put("inReplyTo", replied_object.data["id"]) + |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) + |> Map.put("inReplyToStatusId", activity.id) + |> Map.put("conversation", 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 -> Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") @@ -102,10 +126,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("tag", combined) end + # content map usually only has one language so this will do for now. + def fix_content_map(%{"contentMap" => content_map} = object) do + content_groups = Map.to_list(content_map) + {_, content} = Enum.at(content_groups, 0) + + object + |> Map.put("content", content) + end + + def fix_content_map(object), do: object + # TODO: validate those with a Ecto scheme # - tags # - emoji - def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do + def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) + when objtype in ["Article", "Note"] do + actor = get_actor(data) + data = Map.put(data, "actor", actor) + with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]), %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do object = fix_object(data["object"]) @@ -312,7 +351,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- 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} else _e -> :error @@ -395,7 +434,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming(_), do: :error def get_obj_helper(id) do - if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil + if object = Object.normalize(id), do: {:ok, object}, else: nil end def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do @@ -443,14 +482,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # 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 + with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, "object" => follow_activity.data["object"], @@ -468,14 +500,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do 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 + with follow_activity <- Activity.normalize(data["object"]) do object = %{ "actor" => follow_activity.actor, "object" => follow_activity.data["object"], diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 64329b710..8b41a3bec 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -128,7 +128,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do Inserts a full object if it is contained in an activity. """ def insert_full_object(%{"object" => %{"type" => type} = object_data}) - when is_map(object_data) and type in ["Note"] do + when is_map(object_data) and type in ["Article", "Note"] do with {:ok, _} <- Object.create(object_data) do :ok end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index f4b2e0610..41bfe5048 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("user.json", %{user: user}) do {:ok, user} = WebFinger.ensure_keys_present(user) {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) - public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) + public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) public_key = :public_key.pem_encode([public_key]) %{ diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 8845419c2..3f18a68e8 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.CommonAPI do def delete(activity_id, user) do with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), - %Object{} = object <- Object.get_by_ap_id(object_id), + %Object{} = object <- Object.normalize(object_id), true <- user.info["is_moderator"] || user.ap_id == object.data["actor"], {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} @@ -16,7 +16,7 @@ defmodule Pleroma.Web.CommonAPI do def repeat(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), - object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.announce(user, object) else _ -> @@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do def unrepeat(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), - object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.unannounce(user, object) else _ -> @@ -37,7 +37,7 @@ defmodule Pleroma.Web.CommonAPI do def favorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), false <- activity.data["actor"] == user.ap_id, - object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.like(user, object) else _ -> @@ -48,7 +48,7 @@ defmodule Pleroma.Web.CommonAPI do def unfavorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), false <- activity.data["actor"] == user.ap_id, - object <- Object.get_by_ap_id(activity.data["object"]["id"]) do + object <- Object.normalize(activity.data["object"]["id"]) do ActivityPub.unlike(user, object) else _ -> diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 1a012c1b4..cbedca004 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -35,7 +35,8 @@ defmodule Pleroma.Web.Endpoint do parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Jason, - length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit) + length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit), + body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} ) plug(Plug.MethodOverride) diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 8ca530031..ccefb0bdf 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -95,7 +95,7 @@ defmodule Pleroma.Web.Federator do params = Utils.normalize_params(params) with {:ok, _user} <- ap_enabled_actor(params["actor"]), - nil <- Activity.get_by_ap_id(params["id"]), + nil <- Activity.normalize(params["id"]), {:ok, _activity} <- Transmogrifier.handle_incoming(params) do else %Activity{} -> diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 922b83ed0..956787d5a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1,6 +1,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - alias Pleroma.{Repo, Activity, User, Notification, Stats} + alias Pleroma.{Repo, Object, Activity, User, Notification, Stats} alias Pleroma.Web alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} alias Pleroma.Web.ActivityPub.ActivityPub @@ -125,7 +125,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do response = %{ uri: Web.base_url(), title: Keyword.get(@instance, :name), - description: "A Pleroma instance, an alternative fediverse server", + description: Keyword.get(@instance, :description), version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})", email: Keyword.get(@instance, :email), urls: %{ @@ -360,7 +360,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end 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 render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}) end @@ -428,16 +428,43 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do render(conn, AccountView, "relationships.json", %{user: user, targets: targets}) end - def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do - with {:ok, object} <- ActivityPub.upload(file) do + def update_media(%{assigns: %{user: _}} = conn, data) do + with %Object{} = object <- Repo.get(Object, data["id"]), + true <- is_binary(data["description"]), + description <- data["description"] do + new_data = %{object.data | "name" => description} + + change = Object.change(object, %{data: new_data}) + {:ok, media_obj} = Repo.update(change) + data = - object.data + new_data |> Map.put("id", object.id) render(conn, StatusView, "attachment.json", %{attachment: data}) end end + def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do + with {:ok, object} <- ActivityPub.upload(file) do + objdata = + if Map.has_key?(data, "description") do + Map.put(object.data, "name", data["description"]) + else + object.data + end + + change = Object.change(object, %{data: objdata}) + {:ok, object} = Repo.update(change) + + objdata = + objdata + |> Map.put("id", object.id) + + render(conn, StatusView, "attachment.json", %{attachment: objdata}) + end + end + def favourited_by(conn, %{"id" => id}) do with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do q = from(u in User, where: u.ap_id in ^likes) @@ -621,6 +648,58 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) end + def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, params["resolve"] == "true") + + fetched = + if Regex.match?(~r/https?:/, query) do + with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do + activities + |> Enum.filter(fn + %{data: %{"type" => "Create"}} -> true + _ -> false + end) + else + _e -> [] + end + end || [] + + q = + from( + a in Activity, + where: fragment("?->>'type' = 'Create'", a.data), + where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, + where: + fragment( + "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", + a.data, + ^query + ), + limit: 20, + order_by: [desc: :id] + ) + + statuses = Repo.all(q) ++ fetched + + tags_path = Web.base_url() <> "/tag/" + + tags = + String.split(query) + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) + + res = %{ + "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), + "statuses" => + StatusView.render("index.json", activities: statuses, for: user, as: :activity), + "hashtags" => tags + } + + json(conn, res) + end + def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do accounts = User.search(query, params["resolve"] == "true") @@ -812,11 +891,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do boost_modal: false, delete_modal: true, auto_play_gif: false, - reduce_motion: false + display_sensitive_media: false, + reduce_motion: false, + max_toot_chars: Keyword.get(@instance, :limit) + }, + rights: %{ + delete_others_notice: !!user.info["is_moderator"] }, compose: %{ me: "#{user.id}", - default_privacy: "public", + default_privacy: user.info["default_scope"] || "public", default_sensitive: false }, media_attachments: %{ diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex index 46648c366..174293906 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_socket.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex @@ -15,8 +15,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket do with token when not is_nil(token) <- params["access_token"], %Token{user_id: user_id} <- Repo.get_by(Token, token: token), %User{} = user <- Repo.get(User, user_id), - stream when stream in ["public", "public:local", "user", "direct", "list"] <- - params["stream"] do + stream + when stream in [ + "public", + "public:local", + "public:media", + "public:local:media", + "user", + "direct", + "list" + ] <- params["stream"] do topic = if stream == "list", do: "list:#{params["list"]}", else: stream socket = diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 9db683f44..cc5261616 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -14,6 +14,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do header = User.banner_url(user) |> MediaProxy.url() user_info = User.user_info(user) + emojis = + (user.info["source_data"]["tag"] || []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + %{ + "shortcode" => String.trim(name, ":"), + "url" => MediaProxy.url(url), + "static_url" => MediaProxy.url(url), + "visible_in_picker" => false + } + end) + %{ id: to_string(user.id), username: hd(String.split(user.nickname, "@")), @@ -30,6 +42,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do avatar_static: image, header: header, header_static: header, + emojis: emojis, + fields: [], source: %{ note: "", privacy: "public", diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d1d48cd0a..5dbd59dd9 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -54,8 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do %{ id: to_string(activity.id), uri: object, - # TODO: This might be wrong, check with mastodon. - url: nil, + url: object, account: AccountView.render("account.json", %{user: user}), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -125,10 +124,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do uri: object["id"], url: object["external_url"] || object["id"], account: AccountView.render("account.json", %{user: user}), - in_reply_to_id: reply_to && reply_to.id, - in_reply_to_account_id: reply_to_user && reply_to_user.id, + in_reply_to_id: reply_to && to_string(reply_to.id), + in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), reblog: nil, - content: HtmlSanitizeEx.basic_html(object["content"]), + content: render_content(object), created_at: created_at, reblogs_count: announcement_count, favourites_count: like_count, @@ -170,7 +169,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do remote_url: href, preview_url: MediaProxy.url(href), text_url: href, - type: type + type: type, + description: attachment["name"] } end @@ -207,4 +207,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do "direct" end end + + def render_content(%{"type" => "Article"} = object) do + summary = object["name"] + + content = + if !!summary and summary != "" do + "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}" + else + object["content"] + end + + HtmlSanitizeEx.basic_html(content) + end + + def render_content(object) do + HtmlSanitizeEx.basic_html(object["content"]) + end end diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index aec77168a..7c67bbf1c 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -4,8 +4,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do alias Pleroma.Stats alias Pleroma.Web - @instance Application.get_env(:pleroma, :instance) - def schemas(conn, _params) do response = %{ links: [ @@ -21,20 +19,22 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json def nodeinfo(conn, %{"version" => "2.0"}) do + instance = Application.get_env(:pleroma, :instance) + media_proxy = Application.get_env(:pleroma, :media_proxy) stats = Stats.get_stats() response = %{ version: "2.0", software: %{ name: "pleroma", - version: Keyword.get(@instance, :version) + version: Keyword.get(instance, :version) }, protocols: ["ostatus", "activitypub"], services: %{ inbound: [], outbound: [] }, - openRegistrations: Keyword.get(@instance, :registrations_open), + openRegistrations: Keyword.get(instance, :registrations_open), usage: %{ users: %{ total: stats.user_count || 0 @@ -42,7 +42,10 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do localPosts: stats.status_count || 0 }, metadata: %{ - nodeName: Keyword.get(@instance, :name) + nodeName: Keyword.get(instance, :name), + nodeDescription: Keyword.get(instance, :description), + mediaProxy: Keyword.get(media_proxy, :enabled), + private: !Keyword.get(instance, :public, true) } } diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 3dd87d0ab..a5fb32a4e 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -81,10 +81,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do # - investigate a way to verify the user wants to grant read/write/follow once scope handling is done def token_exchange( conn, - %{"grant_type" => "password", "name" => name, "password" => password} = params + %{"grant_type" => "password", "username" => name, "password" => password} = params ) do 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), {:ok, auth} <- Authorization.create_authorization(app, user), {:ok, token} <- Token.exchange_token(app, auth) do @@ -104,6 +104,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do 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 token |> Base.url_decode64!(padding: false) diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 4179d86c9..5d831459b 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -246,7 +246,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = (activity.recipients || []) |> get_mentions - follow_activity = Activity.get_by_ap_id(follow_activity["id"]) + follow_activity = Activity.normalize(follow_activity) [ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, diff --git a/lib/pleroma/web/ostatus/handlers/delete_handler.ex b/lib/pleroma/web/ostatus/handlers/delete_handler.ex index 4f3016b65..6330d7f64 100644 --- a/lib/pleroma/web/ostatus/handlers/delete_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/delete_handler.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandler do def handle_delete(entry, _doc \\ nil) do with id <- XML.string_from_xpath("//id", entry), - object when not is_nil(object) <- Object.get_by_ap_id(id), + %Object{} = object <- Object.normalize(id), {:ok, delete} <- ActivityPub.delete(object, false) do delete end diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index f0ff0624f..916c894eb 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -89,7 +89,7 @@ defmodule Pleroma.Web.OStatus do def make_share(entry, doc, retweeted_activity) do with {:ok, actor} <- find_make_or_update_user(doc), - %Object{} = object <- Object.get_by_ap_id(retweeted_activity.data["object"]["id"]), + %Object{} = object <- Object.normalize(retweeted_activity.data["object"]), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do {:ok, activity} @@ -107,7 +107,7 @@ defmodule Pleroma.Web.OStatus do def make_favorite(entry, doc, favorited_activity) do with {:ok, actor} <- find_make_or_update_user(doc), - %Object{} = object <- Object.get_by_ap_id(favorited_activity.data["object"]["id"]), + %Object{} = object <- Object.normalize(favorited_activity.data["object"]), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do {:ok, activity} diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 2f72fdb16..09d1b1110 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Repo alias Pleroma.Web.{OStatus, Federator} alias Pleroma.Web.XML + alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPub @@ -90,7 +91,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do case get_format(conn) do "html" -> redirect(conn, to: "/notice/#{activity.id}") - _ -> represent_activity(conn, activity, user) + _ -> represent_activity(conn, nil, activity, user) end else {:public?, false} -> @@ -107,12 +108,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do def activity(conn, %{"uuid" => uuid}) do with id <- o_status_url(conn, :activity, uuid), - {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(id)}, + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, true} <- {:public?, ActivityPub.is_public?(activity)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - case get_format(conn) do + case format = get_format(conn) do "html" -> redirect(conn, to: "/notice/#{activity.id}") - _ -> represent_activity(conn, activity, user) + _ -> represent_activity(conn, format, activity, user) end else {:public?, false} -> @@ -130,14 +131,14 @@ defmodule Pleroma.Web.OStatus.OStatusController do with {_, %Activity{} = activity} <- {:activity, Repo.get(Activity, id)}, {_, true} <- {:public?, ActivityPub.is_public?(activity)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - case get_format(conn) do + case format = get_format(conn) do "html" -> conn |> put_resp_content_type("text/html") |> send_file(200, "priv/static/index.html") _ -> - represent_activity(conn, activity, user) + represent_activity(conn, format, activity, user) end else {:public?, false} -> @@ -151,7 +152,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do end end - defp represent_activity(conn, activity, user) do + defp represent_activity(conn, "activity+json", activity, user) do + conn + |> put_resp_header("content-type", "application/activity+json") + |> json(ObjectView.render("object.json", %{object: activity})) + end + + defp represent_activity(conn, _, activity, user) do response = activity |> ActivityRepresenter.to_simple_form(user, true) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index dcbf7f008..be0c10ea4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -41,7 +41,7 @@ defmodule Pleroma.Web.Router do end pipeline :well_known do - plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"]) + plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"]) end pipeline :config do @@ -127,6 +127,7 @@ defmodule Pleroma.Web.Router do get("/notifications/:id", MastodonAPIController, :get_notification) post("/media", MastodonAPIController, :upload) + put("/media/:id", MastodonAPIController, :update_media) get("/lists", MastodonAPIController, :get_lists) get("/lists/:id", MastodonAPIController, :get_list) @@ -170,9 +171,16 @@ defmodule Pleroma.Web.Router do get("/accounts/:id/following", MastodonAPIController, :following) get("/accounts/:id", MastodonAPIController, :user) + get("/trends", MastodonAPIController, :empty_array) + get("/search", MastodonAPIController, :search) end + scope "/api/v2", Pleroma.Web.MastodonAPI do + pipe_through(:api) + get("/search", MastodonAPIController, :search2) + end + scope "/api", Pleroma.Web do pipe_through(:config) @@ -242,6 +250,7 @@ defmodule Pleroma.Web.Router do post("/statuses/update", TwitterAPI.Controller, :status_update) post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet) + post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet) post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post) get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests) @@ -266,6 +275,7 @@ defmodule Pleroma.Web.Router do get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array) get("/mutes/users/ids", TwitterAPI.Controller, :empty_array) + get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array) get("/externalprofile/show", TwitterAPI.Controller, :external_profile) end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 0c9e95485..c61bad830 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -1,7 +1,7 @@ defmodule Pleroma.Web.Streamer do use GenServer require Logger - alias Pleroma.{User, Notification} + alias Pleroma.{User, Notification, Activity, Object} def init(args) do {:ok, args} @@ -138,6 +138,34 @@ defmodule Pleroma.Web.Streamer do {:noreply, state} 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.normalize(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 Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. @@ -145,20 +173,7 @@ defmodule Pleroma.Web.Streamer do blocks = user.info["blocks"] || [] unless item.actor in blocks do - json = - %{ - event: "update", - payload: - Pleroma.Web.MastodonAPI.StatusView.render( - "status.json", - activity: item, - for: user - ) - |> Jason.encode!() - } - |> Jason.encode!() - - send(socket.transport_pid, {:text, json}) + send(socket.transport_pid, {:text, represent_update(item, user)}) end end) end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 7a0c37ce9..24ebdf007 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -99,6 +99,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do conn |> render("followed.html", %{error: false}) else + # Was already following user + {:error, "Could not follow user:" <> _rest} -> + render(conn, "followed.html", %{error: false}) + _e -> conn |> render("follow_login.html", %{ @@ -117,6 +121,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do conn |> render("followed.html", %{error: false}) else + # Was already following user + {:error, "Could not follow user:" <> _rest} -> + conn + |> render("followed.html", %{error: false}) + e -> Logger.debug("Remote follow failed with error #{inspect(e)}") @@ -126,6 +135,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end @instance Application.get_env(:pleroma, :instance) + @instance_fe Application.get_env(:pleroma, :fe) + @instance_chat Application.get_env(:pleroma, :chat) def config(conn, _params) do case get_format(conn) do "xml" -> @@ -148,9 +159,24 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do json(conn, %{ site: %{ name: Keyword.get(@instance, :name), + description: Keyword.get(@instance, :description), server: Web.base_url(), textlimit: to_string(Keyword.get(@instance, :limit)), - closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1") + closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"), + private: if(Keyword.get(@instance, :public, true), do: "0", else: "1"), + pleromafe: %{ + theme: Keyword.get(@instance_fe, :theme), + background: Keyword.get(@instance_fe, :background), + logo: Keyword.get(@instance_fe, :logo), + redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login), + redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login), + chatDisabled: !Keyword.get(@instance_chat, :enabled), + showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel), + showWhoToFollowPanel: Keyword.get(@instance_fe, :show_who_to_follow_panel), + scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled), + whoToFollowProvider: Keyword.get(@instance_fe, :who_to_follow_provider), + whoToFollowLink: Keyword.get(@instance_fe, :who_to_follow_link) + } } }) end diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex index 57837205e..26bfb79af 100644 --- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex +++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter alias Pleroma.{Activity, User} - alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView} + alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView} alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Formatter @@ -164,14 +164,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags - summary = activity.data["object"]["summary"] - - content = - if !!summary and summary != "" do - "<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>" - else - content - end + {summary, content} = ActivityView.render_content(object) html = HtmlSanitizeEx.basic_html(content) @@ -198,7 +191,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do "tags" => tags, "activity_type" => "post", "possibly_sensitive" => possibly_sensitive, - "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object) + "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), + "summary" => object["summary"] } end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index ce0da3340..9d8815ce7 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -14,14 +14,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do end def delete(%User{} = user, id) do - # TwitterAPI does not have an "unretweet" endpoint; instead this is done - # via the "destroy" endpoint. Therefore, we need to handle - # when the status to "delete" is actually an Announce (repeat) object. - with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id) do - case type do - "Announce" -> unrepeat(user, id) - _ -> CommonAPI.delete(id, user) - end + with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id), + {:ok, activity} <- CommonAPI.delete(id, user) do + {:ok, activity} end end @@ -72,8 +67,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do end end - defp unrepeat(%User{} = user, ap_id_or_id) do - with {:ok, _unannounce, activity, _object} <- CommonAPI.unrepeat(ap_id_or_id, user) do + def unrepeat(%User{} = user, ap_id_or_id) 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} end end diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index b29687df5..65e67396b 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -241,6 +241,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do 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 with {:ok, user} <- TwitterAPI.register_user(params) do render(conn, UserView, "show.json", %{user: user}) @@ -397,6 +404,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do json(conn, Jason.encode!([])) end + def raw_empty_array(conn, _params) do + json(conn, []) + end + def update_profile(%{assigns: %{user: user}} = conn, params) do params = if bio = params["description"] do @@ -420,6 +431,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do user end + user = + if default_scope = params["default_scope"] do + with new_info <- Map.put(user.info, "default_scope", default_scope), + 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), {:ok, user} <- User.update_and_set_cache(changeset) do CommonAPI.update(user) diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index 62ce3b7b5..55b5287f5 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -228,15 +228,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags - summary = activity.data["object"]["summary"] - content = object["content"] - - content = - if !!summary and summary != "" do - "<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>" - else - content - end + {summary, content} = render_content(object) html = HtmlSanitizeEx.basic_html(content) @@ -263,7 +255,41 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do "tags" => tags, "activity_type" => "post", "possibly_sensitive" => possibly_sensitive, - "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object) + "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), + "summary" => summary } end + + def render_content(%{"type" => "Note"} = object) do + summary = object["summary"] + + content = + if !!summary and summary != "" do + "<p>#{summary}</p>#{object["content"]}" + else + object["content"] + end + + {summary, content} + end + + def render_content(%{"type" => "Article"} = object) do + summary = object["name"] || object["summary"] + + content = + if !!summary and summary != "" do + "<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}" + else + object["content"] + end + + {summary, content} + end + + def render_content(object) do + summary = object["summary"] || "Unhandled activity type: #{object["type"]}" + content = "<p>#{summary}</p>#{object["content"]}" + + {summary, content} + end end diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index 711008973..7d0f0e703 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -1,6 +1,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do use Pleroma.Web, :view alias Pleroma.User + alias Pleroma.Formatter alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MediaProxy @@ -28,9 +29,19 @@ defmodule Pleroma.Web.TwitterAPI.UserView do user_info = User.get_cached_user_info(user) + emoji = + (user.info["source_data"]["tag"] || []) + |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) + |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> + {String.trim(name, ":"), url} + end) + + bio = HtmlSanitizeEx.strip_tags(user.bio) + data = %{ "created_at" => user.inserted_at |> Utils.format_naive_asctime(), - "description" => HtmlSanitizeEx.strip_tags(user.bio), + "description" => bio, + "description_html" => bio |> Formatter.emojify(emoji), "favourites_count" => 0, "followers_count" => user_info[:follower_count], "following" => following, @@ -39,6 +50,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "friends_count" => user_info[:following_count], "id" => user.id, "name" => user.name, + "name_html" => HtmlSanitizeEx.strip_tags(user.name) |> Formatter.emojify(emoji), "profile_image_url" => image, "profile_image_url_https" => image, "profile_image_url_profile_size" => image, @@ -52,7 +64,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "cover_photo" => User.banner_url(user) |> MediaProxy.url(), "background_image" => image_url(user.info["background"]) |> MediaProxy.url(), "is_local" => user.local, - "locked" => !!user.info["locked"] + "locked" => !!user.info["locked"], + "default_scope" => user.info["default_scope"] || "public" } if assigns[:token] do diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index e7ee810f9..9f554d286 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -25,35 +25,17 @@ defmodule Pleroma.Web.WebFinger do |> XmlBuilder.to_doc() end - def webfinger(resource, "JSON") do + def webfinger(resource, fmt) when fmt in ["XML", "JSON"] 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, "JSON")} + with %{"username" => username} <- Regex.named_captures(regex, resource), + %User{} = user <- User.get_by_nickname(username) do + {:ok, represent_user(user, fmt)} else _e -> - with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do - {:ok, represent_user(user, "JSON")} - 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")} + with %User{} = user <- User.get_cached_by_ap_id(resource) do + {:ok, represent_user(user, fmt)} else _e -> {:error, "Couldn't find user"} |