Merge develop

This commit is contained in:
Roman Chvanikov 2019-07-16 16:19:19 +03:00
commit 9bca70b10a
51 changed files with 1157 additions and 213 deletions

View file

@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
- Mastodon API: Unsubscribe followers when they unfollow a user
### Fixed ### Fixed
- Not being able to pin unlisted posts - Not being able to pin unlisted posts
@ -29,6 +30,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API, extension: Ability to reset avatar, profile banner, and background - Mastodon API, extension: Ability to reset avatar, profile banner, and background
- Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196> - Mastodon API: Add support for categories for custom emojis by reusing the group feature. <https://github.com/tootsuite/mastodon/pull/11196>
- Mastodon API: Add support for muting/unmuting notifications - Mastodon API: Add support for muting/unmuting notifications
- Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). <https://github.com/tootsuite/mastodon/pull/10373>
- Mastodon API: Add `pleroma.deactivated` to the Account entity
- Admin API: Return users' tags when querying reports - Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users - Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID - Admin API: Allow querying user by ID
@ -36,12 +39,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added synchronization of following/followers counters for external users - Added synchronization of following/followers counters for external users
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
- Addressable lists
### Changed ### Changed
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Admin API: changed json structure for saving config settings. - Admin API: changed json structure for saving config settings.
- RichMedia: parsers and their order are configured in `rich_media` config. - RichMedia: parsers and their order are configured in `rich_media` config.
## [1.0.1] - 2019-07-14
### Security
- OStatus: fix an object spoofing vulnerability.
## [1.0.0] - 2019-06-29 ## [1.0.0] - 2019-06-29
### Security ### Security
- Mastodon API: Fix display names not being sanitized - Mastodon API: Fix display names not being sanitized

View file

@ -16,9 +16,11 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
## Statuses ## Statuses
- `visibility`: has an additional possible value `list`
Has these additional fields under the `pleroma` object: 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) - `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) - `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` - `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`
@ -45,6 +47,7 @@ Has these additional fields under the `pleroma` object:
- `hide_follows`: boolean, true when the user has follow hiding enabled - `hide_follows`: boolean, true when the user has follow hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials` - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials` - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
- `deactivated`: boolean, true when the user is deactivated
### Source ### Source
@ -72,6 +75,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. - `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. - `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. - `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` ## PATCH `/api/v1/update_credentials`

View file

@ -202,7 +202,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -200,7 +200,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -264,7 +264,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -190,7 +190,6 @@ sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress
#### Further reading #### Further reading
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -180,7 +180,6 @@ mix set_moderator username [true|false]
#### コンフィギュレーションとカスタマイズ #### コンフィギュレーションとカスタマイズ
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -283,7 +283,6 @@ If you opted to allow sudo for the `pleroma` user but would like to remove the a
#### Further reading #### Further reading
* [Admin tasks](Admin tasks)
* [Backup your instance](backup.html) * [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html) * [Configuration tips](general-tips-for-customizing-pleroma-fe.html)
* [Hardening your instance](hardening.html) * [Hardening your instance](hardening.html)

View file

@ -28,6 +28,14 @@ def run(["migrate_to_db"]) do
|> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end)
|> Enum.each(fn {k, v} -> |> Enum.each(fn {k, v} ->
key = to_string(k) |> String.replace("Elixir.", "") 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}) {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v})
Mix.shell().info("#{key} is migrated.") Mix.shell().info("#{key} is migrated.")
end) end)
@ -53,17 +61,9 @@ def run(["migrate_from_db", env, delete?]) do
Repo.all(Config) Repo.all(Config)
|> Enum.each(fn config -> |> Enum.each(fn config ->
mark =
if String.starts_with?(config.key, "Pleroma.") or
String.starts_with?(config.key, "Ueberauth"),
do: ",",
else: ":"
IO.write( IO.write(
file, file,
"config :#{config.group}, #{config.key}#{mark} #{ "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n"
inspect(Config.from_binary(config.value))
}\r\n"
) )
if delete? do if delete? do

View file

@ -35,7 +35,7 @@ defp update_env(setting) do
if String.starts_with?(setting.key, "Pleroma.") do if String.starts_with?(setting.key, "Pleroma.") do
"Elixir." <> setting.key "Elixir." <> setting.key
else else
setting.key String.trim_leading(setting.key, ":")
end end
group = String.to_existing_atom(setting.group) group = String.to_existing_atom(setting.group)

View file

@ -35,10 +35,12 @@ def generate_rsa_pem do
end end
def keys_from_pem(pem) do def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem) with [private_key_code] <- :public_key.pem_decode(pem),
private_key = :public_key.pem_entry_decode(private_key_code) private_key <- :public_key.pem_entry_decode(private_key_code),
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do
public_key = {:RSAPublicKey, modulus, exponent} {:ok, private_key, {:RSAPublicKey, modulus, exponent}}
{:ok, private_key, public_key} else
error -> {:error, error}
end
end end
end end

View file

@ -16,6 +16,7 @@ defmodule Pleroma.List do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: Pleroma.FlakeId)
field(:title, :string) field(:title, :string)
field(:following, {:array, :string}, default: []) field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
timestamps() timestamps()
end end
@ -55,6 +56,10 @@ def get(id, %{id: user_id} = _user) do
Repo.one(query) Repo.one(query)
end 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 def get_following(%Pleroma.List{following: following} = _list) do
q = q =
from( from(
@ -105,7 +110,14 @@ def rename(%Pleroma.List{} = list, title) do
def create(title, %User{} = creator) do def create(title, %User{} = creator) do
list = %Pleroma.List{user_id: creator.id, title: title} 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 end
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
@ -125,4 +137,19 @@ def update_follows(%Pleroma.List{} = list, attrs) do
|> follow_changeset(attrs) |> follow_changeset(attrs)
|> Repo.update() |> Repo.update()
end 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 end

View file

@ -67,7 +67,7 @@ def for_user_query(user, opts \\ []) do
|> join(:left, [n, a], tm in Pleroma.ThreadMute, |> join(:left, [n, a], tm in Pleroma.ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
) )
|> where([n, a, o, tm], is_nil(tm.id)) |> where([n, a, o, tm], is_nil(tm.user_id))
end end
end end

View file

@ -48,6 +48,9 @@ def contain_origin(id, %{"actor" => _actor} = params) do
end end
end end
def contain_origin(id, %{"attributedTo" => actor} = params),
do: contain_origin(id, Map.put(params, "actor", actor))
def contain_origin_from_id(_id, %{"id" => nil}), do: :error def contain_origin_from_id(_id, %{"id" => nil}), do: :error
def contain_origin_from_id(id, %{"id" => other_id} = _params) do def contain_origin_from_id(id, %{"id" => other_id} = _params) do
@ -60,4 +63,9 @@ def contain_origin_from_id(id, %{"id" => other_id} = _params) do
:error :error
end end
end end
def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}),
do: contain_origin(id, object)
def contain_child(_), do: :ok
end end

View file

@ -6,11 +6,26 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
import Plug.Conn import Plug.Conn
alias Pleroma.User alias Pleroma.User
require Logger
def init(options) do def init(options) do
options options
end end
def checkpw(password, password_hash) do
cond do
String.starts_with?(password_hash, "$pbkdf2") ->
Pbkdf2.checkpw(password, password_hash)
String.starts_with?(password_hash, "$6") ->
:crypt.crypt(password, password_hash) == password_hash
true ->
Logger.error("Password hash not recognized")
false
end
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call( def call(

View file

@ -1191,10 +1191,12 @@ def public_key_from_info(%{
end end
# OStatus Magic Key # 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)} {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end end
def public_key_from_info(_), do: {:error, "not found key"}
def get_public_key_for_ap_id(ap_id) do def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do {:ok, public_key} <- public_key_from_info(user.info) do
@ -1454,23 +1456,16 @@ def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
} }
end end
def ensure_keys_present(user) do def ensure_keys_present(%User{info: info} = user) do
info = user.info
if info.keys do if info.keys do
{:ok, user} {:ok, user}
else else
{:ok, pem} = Keys.generate_rsa_pem() {:ok, pem} = Keys.generate_rsa_pem()
info_cng = user
info |> Ecto.Changeset.change()
|> User.Info.set_keys(pem) |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
|> update_and_set_cache()
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
@ -26,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# For Announce activities, we filter the recipients based on following status for any actors # 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. # that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do defp get_recipients(%{"type" => "Announce"} = data) do
to = data["to"] || [] to = Map.get(data, "to", [])
cc = data["cc"] || [] cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"]) actor = User.get_cached_by_ap_id(data["actor"])
recipients = recipients =
(to ++ cc) Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
|> Enum.filter(fn recipient ->
case User.get_cached_by_ap_id(recipient) do case User.get_cached_by_ap_id(recipient) do
nil -> nil -> true
true user -> User.following?(user, actor)
user ->
User.following?(user, actor)
end end
end) end)
@ -46,17 +44,19 @@ defp get_recipients(%{"type" => "Announce"} = data) do
end end
defp get_recipients(%{"type" => "Create"} = data) do defp get_recipients(%{"type" => "Create"} = data) do
to = data["to"] || [] to = Map.get(data, "to", [])
cc = data["cc"] || [] cc = Map.get(data, "cc", [])
actor = data["actor"] || [] bcc = Map.get(data, "bcc", [])
recipients = (to ++ cc ++ [actor]) |> Enum.uniq() actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc} {recipients, to, cc}
end end
defp get_recipients(data) do defp get_recipients(data) do
to = data["to"] || [] to = Map.get(data, "to", [])
cc = data["cc"] || [] cc = Map.get(data, "cc", [])
recipients = to ++ cc bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc} {recipients, to, cc}
end end
@ -126,6 +126,7 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do
{:ok, map} <- MRF.filter(map), {:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map), {recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients}, {:fake, false, map, recipients} <- {:fake, fake, map, recipients},
:ok <- Containment.contain_child(map),
{:ok, map, object} <- insert_full_object(map) do {:ok, map, object} <- insert_full_object(map) do
{:ok, activity} = {:ok, activity} =
Repo.insert(%Activity{ Repo.insert(%Activity{
@ -896,13 +897,11 @@ defp maybe_order(query, %{order: :asc}) do
defp maybe_order(query, _), do: query defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from(activity in Activity)
config = %{ config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment]) skip_thread_containment: Config.get([:instance, :skip_thread_containment])
} }
base_query Activity
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts) |> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
@ -931,11 +930,31 @@ def fetch_activities_query(recipients, opts \\ %{}) do
end end
def fetch_activities(recipients, opts \\ %{}) do 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) |> Pagination.fetch_paginated(opts)
|> Enum.reverse() |> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end 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 def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query, from(activity in query,
where: where:

View file

@ -92,18 +92,68 @@ defp should_federate?(inbox, public) do
end end
end end
@doc """ defp recipients(actor, activity) do
Publishes an activity to all relevant peers. followers =
"""
def publish(%User{} = actor, %Activity{} = activity) do
remote_followers =
if actor.follower_address in activity.recipients do if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor) {:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local)) Enum.filter(followers, &(!&1.local))
else else
[] []
end 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, 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) public = is_public?(activity)
if public && Config.get([:instance, :allow_relay]) do if public && Config.get([:instance, :allow_relay]) do
@ -114,7 +164,7 @@ def publish(%User{} = actor, %Activity{} = activity) do
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(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.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{source_data: data}} -> |> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]

View file

@ -814,13 +814,16 @@ def prepare_object(object) do
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
object = object =
Object.normalize(object_id).data object_id
|> Object.normalize()
|> Map.get(:data)
|> prepare_object |> prepare_object
data = data =
data data
|> Map.put("object", object) |> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data} {:ok, data}
end end

View file

@ -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, # Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have. # so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do def get_ap_id(%{"id" => id} = _), do: id
case object do def get_ap_id(id), do: id
%{"id" => id} -> id
id -> id
end
end
def normalize_params(params) do def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"])) Map.put(params, "actor", get_ap_id(params["actor"]))

View file

@ -34,6 +34,20 @@ def is_direct?(activity) do
!is_public?(activity) && !is_private?(activity) !is_public?(activity) && !is_private?(activity)
end 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 def visible_for_user?(activity, nil) do
is_public?(activity) is_public?(activity)
end end
@ -73,6 +87,9 @@ def get_visibility(object) do
object.data["directMessage"] == true -> object.data["directMessage"] == true ->
"direct" "direct"
is_binary(object.data["listMessage"]) ->
"list"
length(cc) > 0 -> length(cc) > 0 ->
"private" "private"

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2 alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Registration alias Pleroma.Registration
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
def get_user(%Plug.Conn{} = conn) do def get_user(%Plug.Conn{} = conn) do
with {:ok, {name, password}} <- fetch_credentials(conn), with {:ok, {name, password}} <- fetch_credentials(conn),
{_, %User{} = user} <- {:user, fetch_user(name)}, {_, %User{} = user} <- {:user, fetch_user(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do
{:ok, user} {:ok, user}
else else
error -> error ->

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
@ -31,7 +30,8 @@ def follow(follower, followed) do
def unfollow(follower, unfollowed) do def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower} {:ok, follower}
end end
end end
@ -175,6 +175,11 @@ def get_visibility(%{"visibility" => visibility}, in_reply_to)
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)} 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 def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to) visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility} {visibility, visibility}
@ -235,19 +240,18 @@ def post(user, %{"status" => status} = data) do
"emoji", "emoji",
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
) do ) do
res = preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
ActivityPub.create( direct? = visibility == "direct"
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
},
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
)
res %{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => direct?}
}
|> maybe_add_list_data(user, visibility)
|> ActivityPub.create(preview?)
else else
{:private_to_public, true} -> {:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")} {:error, dgettext("errors", "The message visibility must be direct")}
@ -351,15 +355,6 @@ def thread_muted?(user, activity) do
end end
end end
def bookmarked?(user, activity) do
with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
true
else
_ ->
false
end
end
def report(user, data) do def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},

View file

@ -6,11 +6,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias Calendar.Strftime alias Calendar.Strftime
alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -100,12 +100,29 @@ def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
end end
end end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to) User.get_ap_ids_by_nicknames(to)
end end
def get_addressed_users(mentioned_users, _), do: mentioned_users 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) def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do when is_list(options) do
%{max_expiration: max_expiration, min_expiration: min_expiration} = %{max_expiration: max_expiration, min_expiration: min_expiration} =
@ -371,7 +388,7 @@ defp shortname(name) do
def confirm_current_password(user, password) do def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id), with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
{:ok, db_user} {:ok, db_user}
else else
_ -> {:error, dgettext("errors", "Invalid password.")} _ -> {:error, dgettext("errors", "Invalid password.")}

View file

@ -693,11 +693,6 @@ def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) |> try_render("status.json", %{activity: activity, for: user, as: :activity})
else
{:error, reason} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => reason})
end end
end end
@ -738,11 +733,6 @@ def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) |> try_render("status.json", %{activity: activity, for: user, as: :activity})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
end end
end end
@ -881,7 +871,7 @@ def get_mascot(%{assigns: %{user: user}} = conn, _params) do
end end
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^likes) q = from(u in User, where: u.ap_id in ^likes)
users = Repo.all(q) users = Repo.all(q)
@ -895,7 +885,7 @@ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
q = from(u in User, where: u.ap_id in ^announces) q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q) users = Repo.all(q)
@ -1651,6 +1641,12 @@ def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Record not found") render_error(conn, :not_found, "Record not found")
end end
def errors(conn, {:error, error_message}) do
conn
|> put_status(:bad_request)
|> json(%{error: error_message})
end
def errors(conn, _) do def errors(conn, _) do
conn conn
|> put_status(:internal_server_error) |> put_status(:internal_server_error)

View file

@ -51,6 +51,7 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target
following: User.following?(user, target), following: User.following?(user, target),
followed_by: User.following?(target, user), followed_by: User.following?(target, user),
blocking: User.blocks?(user, target), blocking: User.blocks?(user, target),
blocked_by: User.blocks?(target, user),
muting: User.mutes?(user, target), muting: User.mutes?(user, target),
muting_notifications: User.muted_notifications?(user, target), muting_notifications: User.muted_notifications?(user, target),
subscribing: User.subscribed_to?(user, target), subscribing: User.subscribed_to?(user, target),
@ -136,6 +137,7 @@ defp do_render("account.json", %{user: user} = opts) do
|> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_notification_settings(user, opts[:for])
|> maybe_put_settings_store(user, opts[:for], opts) |> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
end end
defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(string) when is_binary(string) do
@ -196,6 +198,12 @@ defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id:
defp maybe_put_notification_settings(data, _, _), do: data defp maybe_put_notification_settings(data, _, _), do: data
defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do
Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated)
end
defp maybe_put_activation_status(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View file

@ -382,7 +382,7 @@ def render("poll.json", %{object: object} = opts) do
%{ %{
# Mastodon uses separate ids for polls, but an object can't have # Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine # more than one poll embedded so object id is fine
id: object.id, id: to_string(object.id),
expires_at: Utils.to_masto_date(end_time), expires_at: Utils.to_masto_date(end_time),
expired: expired, expired: expired,
multiple: multiple, multiple: multiple,

View file

@ -3,68 +3,71 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy do defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
alias Pleroma.Web
@base64_opts [padding: false] @base64_opts [padding: false]
def url(nil), do: nil def url(url) when is_nil(url) or url == "", do: nil
def url(""), do: nil
def url("/" <> _ = url), do: url def url("/" <> _ = url), do: url
def url(url) do def url(url) do
if !enabled?() or local?(url) or whitelisted?(url) do if disabled?() or local?(url) or whitelisted?(url) do
url url
else else
encode_url(url) encode_url(url)
end end
end end
defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false) defp disabled?, do: !Config.get([:media_proxy, :enabled], false)
defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url())
defp whitelisted?(url) do defp whitelisted?(url) do
%{host: domain} = URI.parse(url) %{host: domain} = URI.parse(url)
Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern ->
String.equivalent?(domain, pattern) String.equivalent?(domain, pattern)
end) end)
end end
def encode_url(url) do def encode_url(url) do
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base])
base64 = Base.url_encode64(url, @base64_opts) base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts) sig64 =
base64
|> signed_url
|> Base.url_encode64(@base64_opts)
build_url(sig64, base64, filename(url)) build_url(sig64, base64, filename(url))
end end
def decode_url(sig, url) do def decode_url(sig, url) do
secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) with {:ok, sig} <- Base.url_decode64(sig, @base64_opts),
sig = Base.url_decode64!(sig, @base64_opts) signature when signature == sig <- signed_url(url) do
local_sig = :crypto.hmac(:sha, secret, url)
if local_sig == sig do
{:ok, Base.url_decode64!(url, @base64_opts)} {:ok, Base.url_decode64!(url, @base64_opts)}
else else
{:error, :invalid_signature} _ -> {:error, :invalid_signature}
end end
end end
defp signed_url(url) do
:crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url)
end
def filename(url_or_path) do def filename(url_or_path) do
if path = URI.parse(url_or_path).path, do: Path.basename(path) if path = URI.parse(url_or_path).path, do: Path.basename(path)
end end
def build_url(sig_base64, url_base64, filename \\ nil) do def build_url(sig_base64, url_base64, filename \\ nil) do
[ [
Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()), Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()),
"proxy", "proxy",
sig_base64, sig_base64,
url_base64, url_base64,
filename filename
] ]
|> Enum.filter(fn value -> value end) |> Enum.filter(& &1)
|> Path.join() |> Path.join()
end end
end end

View file

@ -13,7 +13,7 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
with config <- Pleroma.Config.get([:media_proxy], []), with config <- Pleroma.Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false), true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64), {:ok, url} <- MediaProxy.decode_url(sig64, url64),
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do :ok <- filename_matches(params, conn.request_path, url) do
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
else else
false -> false ->
@ -27,13 +27,20 @@ def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
end end
end end
def filename_matches(has_filename, path, url) do def filename_matches(%{"filename" => _} = _, path, url) do
filename = url |> MediaProxy.filename() filename = MediaProxy.filename(url)
if has_filename && filename && Path.basename(path) != filename do if filename && does_not_match(path, filename) do
{:wrong_filename, filename} {:wrong_filename, filename}
else else
:ok :ok
end end
end end
def filename_matches(_, _, _), do: :ok
defp does_not_match(path, filename) do
basename = Path.basename(path)
basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename
end
end end

View file

@ -123,11 +123,26 @@ def encode(private_key, doc) do
{:ok, salmon} {:ok, salmon}
end end
def remote_users(%{data: %{"to" => to} = data}) do def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || []) cc = Map.get(data, "cc", [])
to bcc =
|> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) 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, cc, bcc]
|> Enum.concat()
|> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user && !user.local end) |> Enum.filter(fn user -> user && !user.local end)
end end
@ -191,7 +206,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
{:ok, private, _} = Keys.keys_from_pem(keys) {:ok, private, _} = Keys.keys_from_pem(keys)
{:ok, feed} = encode(private, feed) {: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) salmon_urls = Enum.map(remote_users, & &1.info.salmon)
reachable_urls_metadata = Instances.filter_reachable(salmon_urls) reachable_urls_metadata = Instances.filter_reachable(salmon_urls)

View file

@ -7,10 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger require Logger
alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Emoji alias Pleroma.Emoji
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -96,7 +96,7 @@ def do_remote_follow(conn, %{
name = followee.nickname name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username), with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- AuthenticationPlug.checkpw(password, user.password_hash),
%User{} = _followed <- User.get_cached_by_id(id), %User{} = _followed <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee), {:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do {:ok, _activity} <- ActivityPub.follow(follower, followee) do

View file

@ -14,7 +14,7 @@ def project do
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
test_coverage: [tool: ExCoveralls], test_coverage: [tool: ExCoveralls],
preferred_cli_env: ["coveralls.html": :test],
# Docs # Docs
name: "Pleroma", name: "Pleroma",
homepage_url: "https://pleroma.social/", homepage_url: "https://pleroma.social/",
@ -95,6 +95,7 @@ defp oauth_deps do
defp deps do defp deps do
[ [
{:phoenix, "~> 1.4.8"}, {:phoenix, "~> 1.4.8"},
{:tzdata, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}, {:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"}, {:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"}, {:phoenix_ecto, "~> 4.0"},

View file

@ -6,7 +6,7 @@
"benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
@ -90,9 +90,9 @@
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},

View file

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

View file

@ -20,6 +20,10 @@
"sensitive": "as:sensitive", "sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#", "litepub": "http://litepub.social/ns#",
"directMessage": "litepub:directMessage", "directMessage": "litepub:directMessage",
"listMessage": {
"@id": "litepub:listMessage",
"@type": "@id"
},
"oauthRegistrationEndpoint": { "oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint", "@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id" "@type": "@id"

View file

@ -113,4 +113,30 @@ test "getting own lists a given user belongs to" do
assert owned_list in lists_2 assert owned_list in lists_2
refute not_owned_list in lists_2 refute not_owned_list in lists_2
end 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 end

View file

@ -68,4 +68,34 @@ test "users cannot be collided through fake direction spoofing attempts" do
"[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}" "[error] Could not decode user at fetch https://n1u.moe/users/rye, {:error, :error}"
end end
end end
describe "containment of children" do
test "contain_child() catches spoofing attempts" do
data = %{
"id" => "http://example.com/whatever",
"type" => "Create",
"object" => %{
"id" => "http://example.net/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.com/~bob"
}
:error = Containment.contain_child(data)
end
test "contain_child() allows correct origins" do
data = %{
"id" => "http://example.org/~alyssa/activities/5678",
"type" => "Create",
"object" => %{
"id" => "http://example.org/~alyssa/activities/1234",
"attributedTo" => "http://example.org/~alyssa"
},
"actor" => "http://example.org/~alyssa"
}
:ok = Containment.contain_child(data)
end
end
end end

View file

@ -5,7 +5,6 @@
defmodule Pleroma.ReverseProxyTest do defmodule Pleroma.ReverseProxyTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
import ExUnit.CaptureLog import ExUnit.CaptureLog
import ExUnit.CaptureLog
import Mox import Mox
alias Pleroma.ReverseProxy alias Pleroma.ReverseProxy
alias Pleroma.ReverseProxy.ClientMock alias Pleroma.ReverseProxy.ClientMock

99
test/signature_test.exs Normal file
View file

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

View file

@ -34,8 +34,8 @@ test "settings are migrated to db" do
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
first_db = Config.get_by_params(%{group: "pleroma", key: "first_setting"}) first_db = Config.get_by_params(%{group: "pleroma", key: ":first_setting"})
second_db = Config.get_by_params(%{group: "pleroma", key: "second_setting"}) second_db = Config.get_by_params(%{group: "pleroma", key: ":second_setting"})
refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"}) refute Config.get_by_params(%{group: "pleroma", key: "Pleroma.Repo"})
assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]] assert Config.from_binary(first_db.value) == [key: "value", key2: [Pleroma.Repo]]
@ -45,13 +45,13 @@ test "settings are migrated to db" do
test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
Config.create(%{ Config.create(%{
group: "pleroma", group: "pleroma",
key: "setting_first", key: ":setting_first",
value: [key: "value", key2: [Pleroma.Activity]] value: [key: "value", key2: [Pleroma.Activity]]
}) })
Config.create(%{ Config.create(%{
group: "pleroma", group: "pleroma",
key: "setting_second", key: ":setting_second",
value: [key: "valu2", key2: [Pleroma.Repo]] value: [key: "valu2", key2: [Pleroma.Repo]]
}) })
@ -61,7 +61,7 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file
assert File.exists?(temp_file) assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file) {:ok, file} = File.read(temp_file)
assert file =~ "config :pleroma, setting_first:" assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, setting_second:" assert file =~ "config :pleroma, :setting_second,"
end end
end end

View file

@ -1190,6 +1190,21 @@ test "it can create a Flag activity" do
end end
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 def data_uri do
File.read!("test/fixtures/avatar_data_uri") File.read!("test/fixtures/avatar_data_uri")
end end

View file

@ -416,6 +416,7 @@ test "it ensures that as:Public activities make it to their followers collection
|> Map.put("attributedTo", user.ap_id) |> Map.put("attributedTo", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"]) |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", []) |> Map.put("cc", [])
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object) data = Map.put(data, "object", object)
@ -439,6 +440,7 @@ test "it ensures that address fields become lists" do
|> Map.put("attributedTo", user.ap_id) |> Map.put("attributedTo", user.ap_id)
|> Map.put("to", nil) |> Map.put("to", nil)
|> Map.put("cc", nil) |> Map.put("cc", nil)
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object) data = Map.put(data, "object", object)
@ -1096,6 +1098,18 @@ test "the directMessage flag is present" do
assert modified["directMessage"] == true assert modified["directMessage"] == true
end 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 end
describe "user upgrade" do describe "user upgrade" do

View file

@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
following = insert(:user) following = insert(:user)
unrelated = insert(:user) unrelated = insert(:user)
{:ok, following} = Pleroma.User.follow(following, user) {:ok, following} = Pleroma.User.follow(following, user)
{:ok, list} = Pleroma.List.create("foo", user)
Pleroma.List.follow(list, unrelated)
{:ok, public} = {:ok, public} =
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"}) CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"})
@ -29,6 +32,12 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
{:ok, unlisted} = {:ok, unlisted} =
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"}) CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"})
{:ok, list} =
CommonAPI.post(user, %{
"status" => "@#{mentioned.nickname}",
"visibility" => "list:#{list.id}"
})
%{ %{
public: public, public: public,
private: private, private: private,
@ -37,29 +46,65 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
user: user, user: user,
mentioned: mentioned, mentioned: mentioned,
following: following, following: following,
unrelated: unrelated unrelated: unrelated,
list: list
} }
end 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) assert Visibility.is_direct?(direct)
refute Visibility.is_direct?(public) refute Visibility.is_direct?(public)
refute Visibility.is_direct?(private) refute Visibility.is_direct?(private)
refute Visibility.is_direct?(unlisted) refute Visibility.is_direct?(unlisted)
assert Visibility.is_direct?(list)
end 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) refute Visibility.is_public?(direct)
assert Visibility.is_public?(public) assert Visibility.is_public?(public)
refute Visibility.is_public?(private) refute Visibility.is_public?(private)
assert Visibility.is_public?(unlisted) assert Visibility.is_public?(unlisted)
refute Visibility.is_public?(list)
end 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?(direct)
refute Visibility.is_private?(public) refute Visibility.is_private?(public)
assert Visibility.is_private?(private) assert Visibility.is_private?(private)
refute Visibility.is_private?(unlisted) 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 end
test "visible_for_user?", %{ test "visible_for_user?", %{
@ -70,7 +115,8 @@ test "visible_for_user?", %{
user: user, user: user,
mentioned: mentioned, mentioned: mentioned,
following: following, following: following,
unrelated: unrelated unrelated: unrelated,
list: list
} do } do
# All visible to author # All visible to author
@ -78,6 +124,7 @@ test "visible_for_user?", %{
assert Visibility.visible_for_user?(private, user) assert Visibility.visible_for_user?(private, user)
assert Visibility.visible_for_user?(unlisted, user) assert Visibility.visible_for_user?(unlisted, user)
assert Visibility.visible_for_user?(direct, user) assert Visibility.visible_for_user?(direct, user)
assert Visibility.visible_for_user?(list, user)
# All visible to a mentioned user # All visible to a mentioned user
@ -85,6 +132,7 @@ test "visible_for_user?", %{
assert Visibility.visible_for_user?(private, mentioned) assert Visibility.visible_for_user?(private, mentioned)
assert Visibility.visible_for_user?(unlisted, mentioned) assert Visibility.visible_for_user?(unlisted, mentioned)
assert Visibility.visible_for_user?(direct, mentioned) assert Visibility.visible_for_user?(direct, mentioned)
assert Visibility.visible_for_user?(list, mentioned)
# DM not visible for just follower # DM not visible for just follower
@ -92,6 +140,7 @@ test "visible_for_user?", %{
assert Visibility.visible_for_user?(private, following) assert Visibility.visible_for_user?(private, following)
assert Visibility.visible_for_user?(unlisted, following) assert Visibility.visible_for_user?(unlisted, following)
refute Visibility.visible_for_user?(direct, following) refute Visibility.visible_for_user?(direct, following)
refute Visibility.visible_for_user?(list, following)
# Public and unlisted visible for unrelated user # Public and unlisted visible for unrelated user
@ -99,6 +148,9 @@ test "visible_for_user?", %{
assert Visibility.visible_for_user?(unlisted, unrelated) assert Visibility.visible_for_user?(unlisted, unrelated)
refute Visibility.visible_for_user?(private, unrelated) refute Visibility.visible_for_user?(private, unrelated)
refute Visibility.visible_for_user?(direct, unrelated) refute Visibility.visible_for_user?(direct, unrelated)
# Visible for a list member
assert Visibility.visible_for_user?(list, unrelated)
end end
test "doesn't die when the user doesn't exist", test "doesn't die when the user doesn't exist",
@ -115,18 +167,24 @@ test "get_visibility", %{
public: public, public: public,
private: private, private: private,
direct: direct, direct: direct,
unlisted: unlisted unlisted: unlisted,
list: list
} do } do
assert Visibility.get_visibility(public) == "public" assert Visibility.get_visibility(public) == "public"
assert Visibility.get_visibility(private) == "private" assert Visibility.get_visibility(private) == "private"
assert Visibility.get_visibility(direct) == "direct" assert Visibility.get_visibility(direct) == "direct"
assert Visibility.get_visibility(unlisted) == "unlisted" assert Visibility.get_visibility(unlisted) == "unlisted"
assert Visibility.get_visibility(list) == "list"
end end
test "get_visibility with directMessage flag" do test "get_visibility with directMessage flag" do
assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
end end
test "get_visibility with listMessage flag" do
assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list"
end
describe "entire_thread_visible_for_user?/2" do describe "entire_thread_visible_for_user?/2" do
test "returns false if not found activity", %{user: user} do test "returns false if not found activity", %{user: user} do
refute Visibility.entire_thread_visible_for_user?(%Activity{}, user) refute Visibility.entire_thread_visible_for_user?(%Activity{}, user)

View file

@ -1720,7 +1720,7 @@ test "settings with nesting map", %{conn: conn} do
configs: [ configs: [
%{ %{
"group" => "pleroma", "group" => "pleroma",
"key" => "key1", "key" => ":key1",
"value" => [ "value" => [
%{"tuple" => [":key2", "some_val"]}, %{"tuple" => [":key2", "some_val"]},
%{ %{
@ -1750,7 +1750,7 @@ test "settings with nesting map", %{conn: conn} do
"configs" => [ "configs" => [
%{ %{
"group" => "pleroma", "group" => "pleroma",
"key" => "key1", "key" => ":key1",
"value" => [ "value" => [
%{"tuple" => [":key2", "some_val"]}, %{"tuple" => [":key2", "some_val"]},
%{ %{
@ -1782,7 +1782,7 @@ test "value as map", %{conn: conn} do
configs: [ configs: [
%{ %{
"group" => "pleroma", "group" => "pleroma",
"key" => "key1", "key" => ":key1",
"value" => %{"key" => "some_val"} "value" => %{"key" => "some_val"}
} }
] ]
@ -1793,7 +1793,7 @@ test "value as map", %{conn: conn} do
"configs" => [ "configs" => [
%{ %{
"group" => "pleroma", "group" => "pleroma",
"key" => "key1", "key" => ":key1",
"value" => %{"key" => "some_val"} "value" => %{"key" => "some_val"}
} }
] ]
@ -1862,6 +1862,45 @@ test "dispatch setting", %{conn: conn} do
] ]
} }
end 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
end end

View file

@ -129,6 +129,37 @@ test "it does not allow replies to direct messages that are not direct messages
}) })
end) end)
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
test "it returns error when status is empty and no attachments" do
user = insert(:user)
assert {:error, "Cannot post an empty status without attachments"} =
CommonAPI.post(user, %{"status" => ""})
end
test "it returns error when character limit is exceeded" do
limit = Pleroma.Config.get([:instance, :limit])
Pleroma.Config.put([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{"status" => "foobar"})
Pleroma.Config.put([:instance, :limit], limit)
end
end end
describe "reactions" do describe "reactions" do
@ -346,6 +377,20 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do
end end
end end
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, followed} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(follower, followed)
refute User.subscribed_to?(follower, followed)
end
end
describe "accept_follow_request/2" do describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do test "after acceptance, it sets all existing pending follow request states to 'accept'" do
user = insert(:user, info: %{locked: true}) user = insert(:user, info: %{locked: true})
@ -387,4 +432,23 @@ test "after rejection, it sets all existing pending follow request states to 're
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end end
end end
describe "vote/3" do
test "does not allow to vote twice" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
})
object = Object.normalize(activity)
{:ok, _, object} = CommonAPI.vote(other_user, object, [0])
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
end
end
end end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
use Pleroma.DataCase use Pleroma.DataCase
import ExUnit.CaptureLog
import Pleroma.Factory import Pleroma.Factory
@public_address "https://www.w3.org/ns/activitystreams#Public" @public_address "https://www.w3.org/ns/activitystreams#Public"
@ -202,7 +203,9 @@ test "when date is a binary in wrong format" do
expected = "" expected = ""
assert Utils.date_to_asctime(date) == expected assert capture_log(fn ->
assert Utils.date_to_asctime(date) == expected
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601"
end end
test "when date is a Unix timestamp" do test "when date is a Unix timestamp" do
@ -210,13 +213,23 @@ test "when date is a Unix timestamp" do
expected = "" expected = ""
assert Utils.date_to_asctime(date) == expected assert capture_log(fn ->
assert Utils.date_to_asctime(date) == expected
end) =~ "[warn] Date #{date} in wrong format, must be ISO 8601"
end end
test "when date is nil" do test "when date is nil" do
expected = "" expected = ""
assert Utils.date_to_asctime(nil) == expected assert capture_log(fn ->
assert Utils.date_to_asctime(nil) == expected
end) =~ "[warn] Date in wrong format, must be ISO 8601"
end
test "when date is a random string" do
assert capture_log(fn ->
assert Utils.date_to_asctime("foo") == ""
end) =~ "[warn] Date foo in wrong format, must be ISO 8601"
end end
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
test "Represent a user account" do test "Represent a user account" do
@ -152,6 +153,13 @@ test "Represent a Service(bot) account" do
assert expected == AccountView.render("account.json", %{user: user}) assert expected == AccountView.render("account.json", %{user: user})
end end
test "Represent a deactivated user for an admin" do
admin = insert(:user, %{info: %{is_admin: true}})
deactivated_user = insert(:user, %{info: %{deactivated: true}})
represented = AccountView.render("account.json", %{user: deactivated_user, for: admin})
assert represented[:pleroma][:deactivated] == true
end
test "Represent a smaller mention" do test "Represent a smaller mention" do
user = insert(:user) user = insert(:user)
@ -165,28 +173,90 @@ test "Represent a smaller mention" do
assert expected == AccountView.render("mention.json", %{user: user}) assert expected == AccountView.render("mention.json", %{user: user})
end end
test "represent a relationship" do describe "relationship" do
user = insert(:user) test "represent a relationship for the following and followed user" do
other_user = insert(:user) user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
{:ok, user} = User.block(user, other_user) {:ok, other_user} = User.follow(other_user, user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.mute(user, other_user, true)
{:ok, user} = CommonAPI.hide_reblogs(user, other_user)
expected = %{ expected = %{
id: to_string(other_user.id), id: to_string(other_user.id),
following: false, following: true,
followed_by: false, followed_by: true,
blocking: true, blocking: false,
muting: false, blocked_by: false,
muting_notifications: false, muting: true,
subscribing: false, muting_notifications: true,
requested: false, subscribing: true,
domain_blocking: false, requested: false,
showing_reblogs: true, domain_blocking: false,
endorsed: false showing_reblogs: false,
} endorsed: false
}
assert expected == AccountView.render("relationship.json", %{user: user, target: other_user}) assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
test "represent a relationship for the blocking and blocked user" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.subscribe(user, other_user)
{:ok, user} = User.block(user, other_user)
{:ok, other_user} = User.block(other_user, user)
expected = %{
id: to_string(other_user.id),
following: false,
followed_by: false,
blocking: true,
blocked_by: true,
muting: false,
muting_notifications: false,
subscribing: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
test "represent a relationship for the user with a pending follow request" do
user = insert(:user)
other_user = insert(:user, %{info: %User.Info{locked: true}})
{:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
expected = %{
id: to_string(other_user.id),
following: false,
followed_by: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
requested: true,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected ==
AccountView.render("relationship.json", %{user: user, target: other_user})
end
end end
test "represent an embedded relationship" do test "represent an embedded relationship" do
@ -240,6 +310,7 @@ test "represent an embedded relationship" do
following: false, following: false,
followed_by: false, followed_by: false,
blocking: true, blocking: true,
blocked_by: false,
subscribing: false, subscribing: false,
muting: false, muting: false,
muting_notifications: false, muting_notifications: false,

View file

@ -35,7 +35,7 @@ test "the home timeline", %{conn: conn} do
user = insert(:user) user = insert(:user)
following = insert(:user) following = insert(:user)
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
conn = conn =
conn conn
@ -58,7 +58,7 @@ test "the public timeline", %{conn: conn} do
following = insert(:user) following = insert(:user)
capture_log(fn -> capture_log(fn ->
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
{:ok, [_activity]} = {:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
@ -1004,8 +1004,8 @@ test "deleting a list", %{conn: conn} do
test "list timeline", %{conn: conn} do test "list timeline", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."}) {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user) {:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, other_user)
@ -1022,10 +1022,10 @@ test "list timeline", %{conn: conn} do
test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity_one} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."}) {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."})
{:ok, _activity_two} = {:ok, _activity_two} =
TwitterAPI.create_status(other_user, %{ CommonAPI.post(other_user, %{
"status" => "Marisa is cute.", "status" => "Marisa is cute.",
"visibility" => "private" "visibility" => "private"
}) })
@ -1049,8 +1049,7 @@ test "list of notifications", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity) {:ok, [_notification]} = Notification.create_notifications(activity)
@ -1072,8 +1071,7 @@ test "getting a single notification", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
@ -1095,8 +1093,7 @@ test "dismissing a single notification", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
@ -1112,8 +1109,7 @@ test "clearing all notifications", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"})
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity) {:ok, [_notification]} = Notification.create_notifications(activity)
@ -1395,6 +1391,17 @@ test "reblogged status for another user", %{conn: conn} do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "returns 400 error when activity is not exist", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/foo/reblog")
assert json_response(conn, 400) == %{"error" => "Could not repeat"}
end
end end
describe "unreblogging" do describe "unreblogging" do
@ -1413,6 +1420,17 @@ test "unreblogs and returns the unreblogged status", %{conn: conn} do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "returns 400 error when activity is not exist", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/foo/unreblog")
assert json_response(conn, 400) == %{"error" => "Could not unrepeat"}
end
end end
describe "favoriting" do describe "favoriting" do
@ -1431,16 +1449,15 @@ test "favs a status and returns it", %{conn: conn} do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "returns 500 for a wrong id", %{conn: conn} do test "returns 400 error for a wrong id", %{conn: conn} do
user = insert(:user) user = insert(:user)
resp = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> post("/api/v1/statuses/1/favourite") |> post("/api/v1/statuses/1/favourite")
|> json_response(500)
assert resp == "Something went wrong" assert json_response(conn, 400) == %{"error" => "Could not favorite"}
end end
end end
@ -1461,6 +1478,17 @@ test "unfavorites a status and returns it", %{conn: conn} do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "returns 400 error for a wrong id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/unfavourite")
assert json_response(conn, 400) == %{"error" => "Could not unfavorite"}
end
end end
describe "user timelines" do describe "user timelines" do
@ -1531,10 +1559,10 @@ test "gets an users media", %{conn: conn} do
media = media =
TwitterAPI.upload(file, user, "json") TwitterAPI.upload(file, user, "json")
|> Poison.decode!() |> Jason.decode!()
{:ok, image_post} = {:ok, image_post} =
TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]}) CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
conn = conn =
conn conn
@ -1857,7 +1885,7 @@ test "hashtag timeline", %{conn: conn} do
following = insert(:user) following = insert(:user)
capture_log(fn -> capture_log(fn ->
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"}) {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"})
{:ok, [_activity]} = {:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
@ -2616,7 +2644,7 @@ test "get instance stats", %{conn: conn} do
insert(:user, %{local: false, nickname: "u@peer1.com"}) insert(:user, %{local: false, nickname: "u@peer1.com"})
insert(:user, %{local: false, nickname: "u@peer2.com"}) insert(:user, %{local: false, nickname: "u@peer2.com"})
{:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"}) {:ok, _} = CommonAPI.post(user, %{"status" => "cofe"})
# Stats should count users with missing or nil `info.deactivated` value # Stats should count users with missing or nil `info.deactivated` value
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
@ -2709,6 +2737,17 @@ test "pin status", %{conn: conn, user: user, activity: activity} do
|> json_response(200) |> json_response(200)
end end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response(conn, 400) == %{"error" => "Could not pin"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user) {:ok, _} = CommonAPI.pin(activity.id, user)
@ -2728,6 +2767,15 @@ test "unpin status", %{conn: conn, user: user, activity: activity} do
|> json_response(200) |> json_response(200)
end end
test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/unpin")
assert json_response(conn, 400) == %{"error" => "Could not unpin"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"}) {:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
@ -2902,6 +2950,17 @@ test "mute conversation", %{conn: conn, user: user, activity: activity} do
|> json_response(200) |> json_response(200)
end end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/mute")
assert json_response(conn, 400) == %{"error" => "conversation is already muted"}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity) {:ok, _} = CommonAPI.add_mute(user, activity)
@ -2946,7 +3005,8 @@ test "submit a report with statuses and comment", %{
|> post("/api/v1/reports", %{ |> post("/api/v1/reports", %{
"account_id" => target_user.id, "account_id" => target_user.id,
"status_ids" => [activity.id], "status_ids" => [activity.id],
"comment" => "bad status!" "comment" => "bad status!",
"forward" => "false"
}) })
|> json_response(200) |> json_response(200)
end end
@ -2979,6 +3039,19 @@ test "comment must be up to the size specified in the config", %{
|> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment})
|> json_response(400) |> json_response(400)
end end
test "returns error when account is not exist", %{
conn: conn,
reporter: reporter,
activity: activity
} do
conn =
conn
|> assign(:user, reporter)
|> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"})
assert json_response(conn, 400) == %{"error" => "Account not found"}
end
end end
describe "link headers" do describe "link headers" do
@ -3338,7 +3411,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c
user2 = insert(:user) user2 = insert(:user)
user3 = insert(:user) user3 = insert(:user)
{:ok, replied_to} = TwitterAPI.create_status(user1, %{"status" => "cofe"}) {:ok, replied_to} = CommonAPI.post(user1, %{"status" => "cofe"})
# Reply to status from another user # Reply to status from another user
conn1 = conn1 =
@ -3503,7 +3576,7 @@ test "returns poll entity for object id", %{conn: conn} do
|> get("/api/v1/polls/#{object.id}") |> get("/api/v1/polls/#{object.id}")
response = json_response(conn, 200) response = json_response(conn, 200)
id = object.id id = to_string(object.id)
assert %{"id" => ^id, "expired" => false, "multiple" => false} = response assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
end end
@ -3603,5 +3676,135 @@ test "does not allow multiple choices on a single-choice question", %{conn: conn
total_items == 1 total_items == 1
end) end)
end end
test "does not allow choice index to be greater than options count", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
})
object = Object.normalize(activity)
conn =
conn
|> assign(:user, other_user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]})
assert json_response(conn, 422) == %{"error" => "Invalid indices"}
end
test "returns 404 error when object is not exist", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/polls/1/votes", %{"choices" => [0]})
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
test "returns 404 when poll is private and not available for user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Am I cute?",
"poll" => %{"options" => ["Yes", "No"], "expires_in" => 20},
"visibility" => "private"
})
object = Object.normalize(activity)
conn =
conn
|> assign(:user, other_user)
|> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]})
assert json_response(conn, 404) == %{"error" => "Record not found"}
end
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
conn =
build_conn()
|> assign(:user, user)
[conn: conn, activity: activity]
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
conn =
build_conn()
|> assign(:user, user)
[conn: conn, activity: activity]
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response(:ok)
assert Enum.empty?(response)
end
end end
end end

View file

@ -423,7 +423,7 @@ test "renders a poll" do
expected = %{ expected = %{
emojis: [], emojis: [],
expired: false, expired: false,
id: object.id, id: to_string(object.id),
multiple: false, multiple: false,
options: [ options: [
%{title: "absolutely!", votes_count: 0}, %{title: "absolutely!", votes_count: 0},
@ -541,4 +541,17 @@ test "embeds a relationship in the account in reposts" do
assert result[:reblog][:account][:pleroma][:relationship] == assert result[:reblog][:account][:pleroma][:relationship] ==
AccountView.render("relationship.json", %{user: user, target: user}) AccountView.render("relationship.json", %{user: user, target: user})
end 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 end

View file

@ -0,0 +1,73 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
use Pleroma.Web.ConnCase
import Mock
alias Pleroma.Config
setup do
media_proxy_config = Config.get([:media_proxy]) || []
on_exit(fn -> Config.put([:media_proxy], media_proxy_config) end)
:ok
end
test "it returns 404 when MediaProxy disabled", %{conn: conn} do
Config.put([:media_proxy, :enabled], false)
assert %Plug.Conn{
status: 404,
resp_body: "Not Found"
} = get(conn, "/proxy/hhgfh/eeeee")
assert %Plug.Conn{
status: 404,
resp_body: "Not Found"
} = get(conn, "/proxy/hhgfh/eeee/fff")
end
test "it returns 403 when signature invalidated", %{conn: conn} do
Config.put([:media_proxy, :enabled], true)
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
path = URI.parse(Pleroma.Web.MediaProxy.encode_url("https://google.fn")).path
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "000")
assert %Plug.Conn{
status: 403,
resp_body: "Forbidden"
} = get(conn, path)
assert %Plug.Conn{
status: 403,
resp_body: "Forbidden"
} = get(conn, "/proxy/hhgfh/eeee")
assert %Plug.Conn{
status: 403,
resp_body: "Forbidden"
} = get(conn, "/proxy/hhgfh/eeee/fff")
end
test "redirects on valid url when filename invalidated", %{conn: conn} do
Config.put([:media_proxy, :enabled], true)
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png")
invalid_url = String.replace(url, "test.png", "test-file.png")
response = get(conn, invalid_url)
html = "<html><body>You are being <a href=\"#{url}\">redirected</a>.</body></html>"
assert response.status == 302
assert response.resp_body == html
end
test "it performs ReverseProxy.call when signature valid", %{conn: conn} do
Config.put([:media_proxy, :enabled], true)
Config.put([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
url = Pleroma.Web.MediaProxy.encode_url("https://google.fn/test.png")
with_mock Pleroma.ReverseProxy,
call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do
assert %Plug.Conn{status: :success} = get(conn, url)
end
end
end

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MediaProxyTest do defmodule Pleroma.Web.MediaProxyTest do
use ExUnit.Case use ExUnit.Case
import Pleroma.Web.MediaProxy import Pleroma.Web.MediaProxy
alias Pleroma.Web.MediaProxy.MediaProxyController alias Pleroma.Web.MediaProxy.MediaProxyController
@ -90,22 +90,39 @@ test "validates signature" do
test "filename_matches preserves the encoded or decoded path" do test "filename_matches preserves the encoded or decoded path" do
assert MediaProxyController.filename_matches( assert MediaProxyController.filename_matches(
true, %{"filename" => "/Hello world.jpg"},
"/Hello world.jpg", "/Hello world.jpg",
"http://pleroma.social/Hello world.jpg" "http://pleroma.social/Hello world.jpg"
) == :ok ) == :ok
assert MediaProxyController.filename_matches( assert MediaProxyController.filename_matches(
true, %{"filename" => "/Hello%20world.jpg"},
"/Hello%20world.jpg", "/Hello%20world.jpg",
"http://pleroma.social/Hello%20world.jpg" "http://pleroma.social/Hello%20world.jpg"
) == :ok ) == :ok
assert MediaProxyController.filename_matches( assert MediaProxyController.filename_matches(
true, %{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"},
"/my%2Flong%2Furl%2F2019%2F07%2FS.jpg", "/my%2Flong%2Furl%2F2019%2F07%2FS.jpg",
"http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg" "http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
) == :ok ) == :ok
assert MediaProxyController.filename_matches(
%{"filename" => "/my%2Flong%2Furl%2F2019%2F07%2FS.jp"},
"/my%2Flong%2Furl%2F2019%2F07%2FS.jp",
"http://pleroma.social/my%2Flong%2Furl%2F2019%2F07%2FS.jpg"
) == {:wrong_filename, "my%2Flong%2Furl%2F2019%2F07%2FS.jpg"}
end
test "encoded url are tried to match for proxy as `conn.request_path` encodes the url" do
# conn.request_path will return encoded url
request_path = "/ANALYSE-DAI-_-LE-STABLECOIN-100-D%C3%89CENTRALIS%C3%89-BQ.jpg"
assert MediaProxyController.filename_matches(
true,
request_path,
"https://mydomain.com/uploads/2019/07/ANALYSE-DAI-_-LE-STABLECOIN-100-DÉCENTRALISÉ-BQ.jpg"
) == :ok
end end
test "uses the configured base_url" do test "uses the configured base_url" do