diff options
26 files changed, 549 insertions, 98 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc8db31c..86c90da0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added synchronization of following/followers counters for external users - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. +- Addressable lists ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 3ee7115cf..d2e9bcc4b 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -16,9 +16,11 @@ Adding the parameter `with_muted=true` to the timeline queries will also return ## Statuses +- `visibility`: has an additional possible value `list` + Has these additional fields under the `pleroma` object: -- `local`: true if the post was made on the local instance. +- `local`: true if the post was made on the local instance - `conversation_id`: the ID of the conversation the status is associated with (if any) - `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any) - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` @@ -72,6 +74,7 @@ Additional parameters can be added to the JSON body/Form data: - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example. - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. +- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. ## PATCH `/api/v1/update_credentials` diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a71bcd447..a7d0fac5d 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -28,6 +28,14 @@ defmodule Mix.Tasks.Pleroma.Config do |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) |> Enum.each(fn {k, v} -> key = to_string(k) |> String.replace("Elixir.", "") + + key = + if String.starts_with?(key, "Pleroma.") do + key + else + ":" <> key + end + {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v}) Mix.shell().info("#{key} is migrated.") end) @@ -53,17 +61,9 @@ defmodule Mix.Tasks.Pleroma.Config do Repo.all(Config) |> Enum.each(fn config -> - mark = - if String.starts_with?(config.key, "Pleroma.") or - String.starts_with?(config.key, "Ueberauth"), - do: ",", - else: ":" - IO.write( file, - "config :#{config.group}, #{config.key}#{mark} #{ - inspect(Config.from_binary(config.value)) - }\r\n" + "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" ) if delete? do diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 3c13a0558..7799b2a78 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Config.TransferTask do if String.starts_with?(setting.key, "Pleroma.") do "Elixir." <> setting.key else - setting.key + String.trim_leading(setting.key, ":") end group = String.to_existing_atom(setting.group) diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex index b7bc7a4da..6dd31d3bd 100644 --- a/lib/pleroma/keys.ex +++ b/lib/pleroma/keys.ex @@ -35,10 +35,12 @@ defmodule Pleroma.Keys do end def keys_from_pem(pem) do - [private_key_code] = :public_key.pem_decode(pem) - private_key = :public_key.pem_entry_decode(private_key_code) - {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key - public_key = {:RSAPublicKey, modulus, exponent} - {:ok, private_key, public_key} + with [private_key_code] <- :public_key.pem_decode(pem), + private_key <- :public_key.pem_entry_decode(private_key_code), + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do + {:ok, private_key, {:RSAPublicKey, modulus, exponent}} + else + error -> {:error, error} + end end end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a5b1cad68..1d320206e 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -16,6 +16,7 @@ defmodule Pleroma.List do belongs_to(:user, User, type: Pleroma.FlakeId) field(:title, :string) field(:following, {:array, :string}, default: []) + field(:ap_id, :string) timestamps() end @@ -55,6 +56,10 @@ defmodule Pleroma.List do Repo.one(query) end + def get_by_ap_id(ap_id) do + Repo.get_by(__MODULE__, ap_id: ap_id) + end + def get_following(%Pleroma.List{following: following} = _list) do q = from( @@ -105,7 +110,14 @@ defmodule Pleroma.List do def create(title, %User{} = creator) do list = %Pleroma.List{user_id: creator.id, title: title} - Repo.insert(list) + + Repo.transaction(fn -> + list = Repo.insert!(list) + + list + |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") + |> Repo.update!() + end) end def follow(%Pleroma.List{following: following} = list, %User{} = followed) do @@ -125,4 +137,19 @@ defmodule Pleroma.List do |> follow_changeset(attrs) |> Repo.update() end + + def memberships(%User{follower_address: follower_address}) do + Pleroma.List + |> where([l], ^follower_address in l.following) + |> select([l], l.ap_id) + |> Repo.all() + end + + def memberships(_), do: [] + + def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do + Enum.member?(following, follower_address) + end + + def member?(_, _), do: false end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 29c87d4a9..ffba3f390 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1190,10 +1190,12 @@ defmodule Pleroma.User do end # OStatus Magic Key - def public_key_from_info(%{magic_key: magic_key}) do + def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do {:ok, Pleroma.Web.Salmon.decode_key(magic_key)} end + def public_key_from_info(_), do: {:error, "not found key"} + def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), {:ok, public_key} <- public_key_from_info(user.info) do @@ -1379,23 +1381,16 @@ defmodule Pleroma.User do } end - def ensure_keys_present(user) do - info = user.info - + def ensure_keys_present(%User{info: info} = user) do if info.keys do {:ok, user} else {:ok, pem} = Keys.generate_rsa_pem() - info_cng = - info - |> User.Info.set_keys(pem) - - cng = - Ecto.Changeset.change(user) - |> Ecto.Changeset.put_embed(:info, info_cng) - - update_and_set_cache(cng) + user + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem)) + |> update_and_set_cache() end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 87963b691..31397b09f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -27,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) actor = User.get_cached_by_ap_id(data["actor"]) recipients = - (to ++ cc) - |> Enum.filter(fn recipient -> + Enum.filter(Enum.concat([to, cc, bcc]), fn recipient -> case User.get_cached_by_ap_id(recipient) do - nil -> - true - - user -> - User.following?(user, actor) + nil -> true + user -> User.following?(user, actor) end end) @@ -47,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp get_recipients(%{"type" => "Create"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] - actor = data["actor"] || [] - recipients = (to ++ cc ++ [actor]) |> Enum.uniq() + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + actor = Map.get(data, "actor", []) + recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq() {recipients, to, cc} end defp get_recipients(data) do - to = data["to"] || [] - cc = data["cc"] || [] - recipients = to ++ cc + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + recipients = Enum.concat([to, cc, bcc]) {recipients, to, cc} end @@ -898,13 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_order(query, _), do: query def fetch_activities_query(recipients, opts \\ %{}) do - base_query = from(activity in Activity) - config = %{ skip_thread_containment: Config.get([:instance, :skip_thread_containment]) } - base_query + Activity |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) @@ -933,11 +930,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def fetch_activities(recipients, opts \\ %{}) do - fetch_activities_query(recipients, opts) + list_memberships = Pleroma.List.memberships(opts["user"]) + + fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts) |> Enum.reverse() + |> maybe_update_cc(list_memberships, opts["user"]) end + defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) + when is_list(list_memberships) and length(list_memberships) > 0 do + Enum.map(activities, fn + %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + if Enum.any?(bcc, &(&1 in list_memberships)) do + update_in(activity.data["cc"], &[user_ap_id | &1]) + else + activity + end + + activity -> + activity + end) + end + + defp maybe_update_cc(activities, _, _), do: activities + def fetch_activities_bounded_query(query, recipients, recipients_with_public) do from(activity in query, where: diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index a05e03263..18145e45f 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -92,18 +92,68 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end end - @doc """ - Publishes an activity to all relevant peers. - """ - def publish(%User{} = actor, %Activity{} = activity) do - remote_followers = + defp recipients(actor, activity) do + followers = if actor.follower_address in activity.recipients do {:ok, followers} = User.get_followers(actor) - followers |> Enum.filter(&(!&1.local)) + Enum.filter(followers, &(!&1.local)) else [] end + Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers + end + + defp get_cc_ap_ids(ap_id, recipients) do + host = Map.get(URI.parse(ap_id), :host) + + recipients + |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) + |> Enum.map(& &1.ap_id) + end + + @doc """ + Publishes an activity with BCC to all relevant peers. + """ + + def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do + public = is_public?(activity) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + recipients = recipients(actor, activity) + + recipients + |> Enum.filter(&User.ap_enabled?/1) + |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + %User{ap_id: ap_id} = + Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + + # Get all the recipients on the same host and add them to cc. Otherwise it a remote + # instance would only accept a first message for the first recipient and ignore the rest. + cc = get_cc_ap_ids(ap_id, recipients) + + json = + data + |> Map.put("cc", cc) + |> Jason.encode!() + + Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + }) + end) + end + + @doc """ + Publishes an activity to all relevant peers. + """ + def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) if public && Config.get([:instance, :allow_relay]) do @@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) json = Jason.encode!(data) - (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) + recipients(actor, activity) |> Enum.filter(fn user -> User.ap_enabled?(user) end) |> Enum.map(fn %{info: %{source_data: data}} -> (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d14490bb5..602ae48e1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -814,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do object = - Object.normalize(object_id).data + object_id + |> Object.normalize() + |> Map.get(:data) |> prepare_object data = data |> Map.put("object", object) |> Map.merge(Utils.make_json_ld_header()) + |> Map.delete("bcc") {:ok, data} end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 4288ea4c8..c146f59d4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -25,12 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do # 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. - def get_ap_id(object) do - case object do - %{"id" => id} -> id - id -> id - end - end + def get_ap_id(%{"id" => id} = _), do: id + def get_ap_id(id), do: id def normalize_params(params) do Map.put(params, "actor", get_ap_id(params["actor"])) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 9908a2e75..2666edc7c 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -34,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do !is_public?(activity) && !is_private?(activity) end + def is_list?(%{data: %{"listMessage" => _}}), do: true + def is_list?(_), do: false + + def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + + def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do + user.ap_id in activity.data["to"] || + list_ap_id + |> Pleroma.List.get_by_ap_id() + |> Pleroma.List.member?(user) + end + + def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false + def visible_for_user?(activity, nil) do is_public?(activity) end @@ -73,6 +87,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do object.data["directMessage"] == true -> "direct" + is_binary(object.data["listMessage"]) -> + "list" + length(cc) > 0 -> "private" diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 949baa3b0..44669b228 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -176,6 +176,11 @@ defmodule Pleroma.Web.CommonAPI do when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} + def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do + visibility = {:list, String.to_integer(list_id)} + {visibility, get_replied_to_visibility(in_reply_to)} + end + def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do visibility = get_replied_to_visibility(in_reply_to) {visibility, visibility} @@ -236,19 +241,18 @@ defmodule Pleroma.Web.CommonAPI do "emoji", Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do - res = - ActivityPub.create( - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => visibility == "direct"} - }, - Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false - ) - - res + preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false + direct? = visibility == "direct" + + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 8e482eef7..f28a96320 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -100,12 +100,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end + def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) end def get_addressed_users(mentioned_users, _), do: mentioned_users + def maybe_add_list_data(activity_params, user, {:list, list_id}) do + case Pleroma.List.get(list_id, user) do + %Pleroma.List{} = list -> + activity_params + |> put_in([:additional, "bcc"], [list.ap_id]) + |> put_in([:additional, "listMessage"], list.ap_id) + |> put_in([:object, "listMessage"], list.ap_id) + + _ -> + activity_params + end + end + + def maybe_add_list_data(activity_params, _, _), do: activity_params + def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) when is_list(options) do %{max_expiration: max_expiration, min_expiration: min_expiration} = diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index e96e4e1e4..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do {:ok, salmon} end - def remote_users(%{data: %{"to" => to} = data}) do - to = to ++ (data["cc"] || []) + def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do + cc = Map.get(data, "cc", []) + + bcc = + data + |> Map.get("bcc", []) + |> Enum.reduce([], fn ap_id, bcc -> + case Pleroma.List.get_by_ap_id(ap_id) do + %Pleroma.List{user_id: ^user_id} = list -> + {:ok, following} = Pleroma.List.get_following(list) + bcc ++ Enum.map(following, & &1.ap_id) + + _ -> + bcc + end + end) - to - |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) + [to, cc, bcc] + |> Enum.concat() + |> Enum.map(&User.get_cached_by_ap_id/1) |> Enum.filter(fn user -> user && !user.local end) end @@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do {:ok, private, _} = Keys.keys_from_pem(keys) {:ok, feed} = encode(private, feed) - remote_users = remote_users(activity) + remote_users = remote_users(user, activity) salmon_urls = Enum.map(remote_users, & &1.info.salmon) reachable_urls_metadata = Instances.filter_reachable(salmon_urls) diff --git a/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs b/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs new file mode 100644 index 000000000..3c32bc355 --- /dev/null +++ b/priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs @@ -0,0 +1,26 @@ +defmodule Pleroma.Repo.Migrations.AddApIdToLists do + use Ecto.Migration + + def up do + alter table(:lists) do + add(:ap_id, :string) + end + + execute(""" + UPDATE lists + SET ap_id = u.ap_id || '/lists/' || lists.id + FROM users AS u + WHERE lists.user_id = u.id + """) + + create(unique_index(:lists, :ap_id)) + end + + def down do + drop(index(:lists, [:ap_id])) + + alter table(:lists) do + remove(:ap_id) + end + end +end diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld index f36b231c5..57ed05eba 100644 --- a/priv/static/schemas/litepub-0.1.jsonld +++ b/priv/static/schemas/litepub-0.1.jsonld @@ -20,6 +20,10 @@ "sensitive": "as:sensitive", "litepub": "http://litepub.social/ns#", "directMessage": "litepub:directMessage", + "listMessage": { + "@id": "litepub:listMessage", + "@type": "@id" + }, "oauthRegistrationEndpoint": { "@id": "litepub:oauthRegistrationEndpoint", "@type": "@id" diff --git a/test/list_test.exs b/test/list_test.exs index 1909c0cd9..f39033d02 100644 --- a/test/list_test.exs +++ b/test/list_test.exs @@ -113,4 +113,30 @@ defmodule Pleroma.ListTest do assert owned_list in lists_2 refute not_owned_list in lists_2 end + + test "get by ap_id" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + assert Pleroma.List.get_by_ap_id(list.ap_id) == list + end + + test "memberships" do + user = insert(:user) + member = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + assert Pleroma.List.memberships(member) == [list.ap_id] + end + + test "member?" do + user = insert(:user) + member = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + assert Pleroma.List.member?(list, member) + refute Pleroma.List.member?(list, user) + end end diff --git a/test/signature_test.exs b/test/signature_test.exs new file mode 100644 index 000000000..4920196c7 --- /dev/null +++ b/test/signature_test.exs @@ -0,0 +1,99 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.SignatureTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Tesla.Mock + + alias Pleroma.Signature + + setup do + mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" + + @public_key %{ + "id" => "https://mastodon.social/users/lambadalambda#main-key", + "owner" => "https://mastodon.social/users/lambadalambda", + "publicKeyPem" => + "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" + } + + @rsa_public_key { + :RSAPublicKey, + 24_650_000_183_914_698_290_885_268_529_673_621_967_457_234_469_123_179_408_466_269_598_577_505_928_170_923_974_132_111_403_341_217_239_999_189_084_572_368_839_502_170_501_850_920_051_662_384_964_248_315_257_926_552_945_648_828_895_432_624_227_029_881_278_113_244_073_644_360_744_504_606_177_648_469_825_063_267_913_017_309_199_785_535_546_734_904_379_798_564_556_494_962_268_682_532_371_146_333_972_821_570_577_277_375_020_977_087_539_994_500_097_107_935_618_711_808_260_846_821_077_839_605_098_669_707_417_692_791_905_543_116_911_754_774_323_678_879_466_618_738_207_538_013_885_607_095_203_516_030_057_611_111_308_904_599_045_146_148_350_745_339_208_006_497_478_057_622_336_882_506_112_530_056_970_653_403_292_123_624_453_213_574_011_183_684_739_084_105_206_483_178_943_532_208_537_215_396_831_110_268_758_639_826_369_857, + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + 65_537 + } + + describe "fetch_public_key/1" do + test "it returns key" do + expected_result = {:ok, @rsa_public_key} + + user = insert(:user, %{info: %{source_data: %{"publicKey" => @public_key}}}) + + assert Signature.fetch_public_key(%Plug.Conn{params: %{"actor" => user.ap_id}}) == + expected_result + end + + test "it returns error when not found user" do + assert Signature.fetch_public_key(%Plug.Conn{params: %{"actor" => "test-ap_id"}}) == + {:error, :error} + end + + test "it returns error if public key is empty" do + user = insert(:user, %{info: %{source_data: %{"publicKey" => %{}}}}) + + assert Signature.fetch_public_key(%Plug.Conn{params: %{"actor" => user.ap_id}}) == + {:error, :error} + end + end + + describe "refetch_public_key/1" do + test "it returns key" do + ap_id = "https://mastodon.social/users/lambadalambda" + + assert Signature.refetch_public_key(%Plug.Conn{params: %{"actor" => ap_id}}) == + {:ok, @rsa_public_key} + end + + test "it returns error when not found user" do + assert Signature.refetch_public_key(%Plug.Conn{params: %{"actor" => "test-ap_id"}}) == + {:error, {:error, :ok}} + end + end + + describe "sign/2" do + test "it returns signature headers" do + user = + insert(:user, %{ + ap_id: "https://mastodon.social/users/lambadalambda", + info: %{keys: @private_key} + }) + + assert Signature.sign( + user, + %{ + host: "test.test", + "content-length": 100 + } + ) == + "keyId=\"https://mastodon.social/users/lambadalambda#main-key\",algorithm=\"rsa-sha256\",headers=\"content-length host\",signature=\"sibUOoqsFfTDerquAkyprxzDjmJm6erYc42W5w1IyyxusWngSinq5ILTjaBxFvfarvc7ci1xAi+5gkBwtshRMWm7S+Uqix24Yg5EYafXRun9P25XVnYBEIH4XQ+wlnnzNIXQkU3PU9e6D8aajDZVp3hPJNeYt1gIPOA81bROI8/glzb1SAwQVGRbqUHHHKcwR8keiR/W2h7BwG3pVRy4JgnIZRSW7fQogKedDg02gzRXwUDFDk0pr2p3q6bUWHUXNV8cZIzlMK+v9NlyFbVYBTHctAR26GIAN6Hz0eV0mAQAePHDY1mXppbA8Gpp6hqaMuYfwifcXmcc+QFm4e+n3A==\"" + end + + test "it returns error" do + user = + insert(:user, %{ap_id: "https://mastodon.social/users/lambadalambda", info: %{keys: ""}}) + + assert Signature.sign( + user, + %{host: "test.test", "content-length": 100} + ) == {:error, []} + end + end +end diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs index bbcc57217..a9b79eb5b 100644 --- a/test/tasks/config_test.exs +++ b/test/tasks/config_test.exs @@ -34,8 +34,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) - first_db = Config.get_by_params(%{group: "pleroma", key: "first_setting"}) - second_db = Config.get_by_params(%{group: "pleroma", key: "second_setting"}) + first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"}) + second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"}) refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"}) assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]] @@ -45,13 +45,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do Config.create(%{ group: "pleroma", - key: "setting_first", + key: ":setting_first", value: [key: "value", key2: [Pleroma.Activity]] }) Config.create(%{ group: "pleroma", - key: "setting_second", + key: ":setting_second", value: [key: "valu2", key2: [Pleroma.Repo]] }) @@ -61,7 +61,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do assert File.exists?(temp_file) {:ok, file} = File.read(temp_file) - assert file =~ "config :pleroma, setting_first:" - assert file =~ "config :pleroma, setting_second:" + assert file =~ "config :pleroma, :setting_first," + assert file =~ "config :pleroma, :setting_second," end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 59d56f3a7..00adbc0f9 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1190,6 +1190,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end + test "fetch_activities/2 returns activities addressed to a list " do + user = insert(:user) + member = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.follow(list, member) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) + + activity = Repo.preload(activity, :bookmark) + activity = %Activity{activity | thread_muted?: !!activity.thread_muted?} + + assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity] + end + def data_uri do File.read!("test/fixtures/avatar_data_uri") end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index cabe925f9..a1f5f6e36 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1098,6 +1098,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert modified["directMessage"] == true end + + test "it strips BCC field" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) + + {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) + + assert is_nil(modified["bcc"]) + end end describe "user upgrade" do diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index 4d5c07da4..b62a89e68 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do following = insert(:user) unrelated = insert(:user) {:ok, following} = Pleroma.User.follow(following, user) + {:ok, list} = Pleroma.List.create("foo", user) + + Pleroma.List.follow(list, unrelated) {:ok, public} = CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"}) @@ -29,6 +32,12 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do {:ok, unlisted} = CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"}) + {:ok, list} = + CommonAPI.post(user, %{ + "status" => "@#{mentioned.nickname}", + "visibility" => "list:#{list.id}" + }) + %{ public: public, private: private, @@ -37,29 +46,65 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do user: user, mentioned: mentioned, following: following, - unrelated: unrelated + unrelated: unrelated, + list: list } end - test "is_direct?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + test "is_direct?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do assert Visibility.is_direct?(direct) refute Visibility.is_direct?(public) refute Visibility.is_direct?(private) refute Visibility.is_direct?(unlisted) + assert Visibility.is_direct?(list) end - test "is_public?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + test "is_public?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do refute Visibility.is_public?(direct) assert Visibility.is_public?(public) refute Visibility.is_public?(private) assert Visibility.is_public?(unlisted) + refute Visibility.is_public?(list) end - test "is_private?", %{public: public, private: private, direct: direct, unlisted: unlisted} do + test "is_private?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do refute Visibility.is_private?(direct) refute Visibility.is_private?(public) assert Visibility.is_private?(private) refute Visibility.is_private?(unlisted) + refute Visibility.is_private?(list) + end + + test "is_list?", %{ + public: public, + private: private, + direct: direct, + unlisted: unlisted, + list: list + } do + refute Visibility.is_list?(direct) + refute Visibility.is_list?(public) + refute Visibility.is_list?(private) + refute Visibility.is_list?(unlisted) + assert Visibility.is_list?(list) end test "visible_for_user?", %{ @@ -70,7 +115,8 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do user: user, mentioned: mentioned, following: following, - unrelated: unrelated + unrelated: unrelated, + list: list } do # All visible to author @@ -78,6 +124,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do assert Visibility.visible_for_user?(private, user) assert Visibility.visible_for_user?(unlisted, user) assert Visibility.visible_for_user?(direct, user) + assert Visibility.visible_for_user?(list, user) # All visible to a mentioned user @@ -85,6 +132,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do assert Visibility.visible_for_user?(private, mentioned) assert Visibility.visible_for_user?(unlisted, mentioned) assert Visibility.visible_for_user?(direct, mentioned) + assert Visibility.visible_for_user?(list, mentioned) # DM not visible for just follower @@ -92,6 +140,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do assert Visibility.visible_for_user?(private, following) assert Visibility.visible_for_user?(unlisted, following) refute Visibility.visible_for_user?(direct, following) + refute Visibility.visible_for_user?(list, following) # Public and unlisted visible for unrelated user @@ -99,6 +148,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do assert Visibility.visible_for_user?(unlisted, unrelated) refute Visibility.visible_for_user?(private, unrelated) refute Visibility.visible_for_user?(direct, unrelated) + + # Visible for a list member + assert Visibility.visible_for_user?(list, unrelated) end test "doesn't die when the user doesn't exist", @@ -115,18 +167,24 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do public: public, private: private, direct: direct, - unlisted: unlisted + unlisted: unlisted, + list: list } do assert Visibility.get_visibility(public) == "public" assert Visibility.get_visibility(private) == "private" assert Visibility.get_visibility(direct) == "direct" assert Visibility.get_visibility(unlisted) == "unlisted" + assert Visibility.get_visibility(list) == "list" end test "get_visibility with directMessage flag" do assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" end + test "get_visibility with listMessage flag" do + assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list" + end + describe "entire_thread_visible_for_user?/2" do test "returns false if not found activity", %{user: user} do refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 1b71cbff3..ee48b752c 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1720,7 +1720,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do configs: [ %{ "group" => "pleroma", - "key" => "key1", + "key" => ":key1", "value" => [ %{"tuple" => [":key2", "some_val"]}, %{ @@ -1750,7 +1750,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "configs" => [ %{ "group" => "pleroma", - "key" => "key1", + "key" => ":key1", "value" => [ %{"tuple" => [":key2", "some_val"]}, %{ @@ -1782,7 +1782,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do configs: [ %{ "group" => "pleroma", - "key" => "key1", + "key" => ":key1", "value" => %{"key" => "some_val"} } ] @@ -1793,7 +1793,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "configs" => [ %{ "group" => "pleroma", - "key" => "key1", + "key" => ":key1", "value" => %{"key" => "some_val"} } ] @@ -1862,6 +1862,45 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do ] } end + + test "queues key as atom", %{conn: conn} do + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + "group" => "pleroma_job_queue", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + }) + + assert json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => "pleroma_job_queue", + "key" => ":queues", + "value" => [ + %{"tuple" => [":federator_incoming", 50]}, + %{"tuple" => [":federator_outgoing", 50]}, + %{"tuple" => [":web_push", 50]}, + %{"tuple" => [":mailer", 10]}, + %{"tuple" => [":transmogrifier", 20]}, + %{"tuple" => [":scheduled_activities", 10]}, + %{"tuple" => [":background", 5]} + ] + } + ] + } + end end end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index b59b6cbf6..7d8eeb12e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -129,6 +129,18 @@ defmodule Pleroma.Web.CommonAPITest do }) end) end + + test "it allows to address a list" do + user = insert(:user) + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) + + assert activity.data["bcc"] == [list.ap_id] + assert activity.recipients == [list.ap_id, user.ap_id] + assert activity.data["listMessage"] == list.ap_id + end end describe "reactions" do diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index ac42819d8..995bd52c8 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -541,4 +541,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert result[:reblog][:account][:pleroma][:relationship] == AccountView.render("relationship.json", %{user: user, target: user}) end + + test "visibility/list" do + user = insert(:user) + + {:ok, list} = Pleroma.List.create("foo", user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"}) + + status = StatusView.render("status.json", activity: activity) + + assert status.visibility == "list" + end end |