Merge remote-tracking branch 'remotes/origin/develop' into authenticated-api-oauth-check-enforcement

This commit is contained in:
Ivan Tashkinov 2020-04-15 19:20:34 +03:00
commit bedf92e064
117 changed files with 2612 additions and 532 deletions

View file

@ -4,24 +4,72 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [unreleased] ## [unreleased]
### Changed
- **Breaking:** BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
### Removed ### Removed
- **Breaking:** removed `with_move` parameter from notifications timeline. - **Breaking:** removed `with_move` parameter from notifications timeline.
### Added ### Added
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list. - NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses. - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required. - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
</details> </details>
### Fixed
- Support pagination in conversations API
## [unreleased-patch]
### Fixed
- Logger configuration through AdminFE
## [2.0.2] - 2020-04-08
### Added
- Support for Funkwhale's `Audio` activity
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/update_credentials`
### Fixed
- Blocked/muted users still generating push notifications
- Input textbox for bio ignoring newlines
- OTP: Inability to use PostgreSQL databases with SSL
- `user delete_activities` breaking when trying to delete already deleted posts
- Incorrect URL for Funkwhale channels
### Upgrade notes
1. Restart Pleroma
## [2.0.1] - 2020-03-15
### Security
- Static-FE: Fix remote posts not being sanitized
### Fixed
- 500 errors when no `Accept` header is present if Static-FE is enabled
- Instance panel not being updated immediately due to wrong `Cache-Control` headers
- Statuses posted with BBCode/Markdown having unncessary newlines in Pleroma-FE
- OTP: Fix some settings not being migrated to in-database config properly
- No `Cache-Control` headers on attachment/media proxy requests
- Character limit enforcement being off by 1
- Mastodon Streaming API: hashtag timelines not working
### Changed
- BBCode and Markdown formatters will no longer return any `\n` and only use `<br/>` for newlines
- Mastodon API: Allow registration without email if email verification is not enabled
### Upgrade notes
#### Nginx only
1. Remove `proxy_ignore_headers Cache-Control;` and `proxy_hide_header Cache-Control;` from your config.
#### Everyone
1. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
2. Restart Pleroma
## [2.0.0] - 2019-03-08 ## [2.0.0] - 2019-03-08
### Security ### Security
- Mastodon API: Fix being able to request enourmous amount of statuses in timelines leading to DoS. Now limited to 40 per request. - Mastodon API: Fix being able to request enormous amount of statuses in timelines leading to DoS. Now limited to 40 per request.
### Removed ### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media` - **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`

View file

@ -1,4 +1,4 @@
Unless otherwise stated this repository is copyright © 2017-2019 Unless otherwise stated this repository is copyright © 2017-2020
Pleroma Authors <https://pleroma.social/>, and is distributed under Pleroma Authors <https://pleroma.social/>, and is distributed under
The GNU Affero General Public License Version 3, you should have received a The GNU Affero General Public License Version 3, you should have received a
copy of the license file as AGPL-3. copy of the license file as AGPL-3.
@ -23,7 +23,7 @@ priv/static/images/pleroma-fox-tan-shy.png
--- ---
The following files are copyright © 2017-2019 Pleroma Authors The following files are copyright © 2017-2020 Pleroma Authors
<https://pleroma.social/>, and are distributed under the Creative Commons <https://pleroma.social/>, and are distributed under the Creative Commons
Attribution-ShareAlike 4.0 International license, you should have received Attribution-ShareAlike 4.0 International license, you should have received
a copy of the license file as CC-BY-SA-4.0. a copy of the license file as CC-BY-SA-4.0.

View file

@ -386,47 +386,56 @@ defp render_timelines(user) do
favourites = ActivityPub.fetch_favourites(user) favourites = ActivityPub.fetch_favourites(user)
output_relationships =
!!Pleroma.Config.get([:extensions, :output_relationships_in_statuses_by_default])
Benchee.run( Benchee.run(
%{ %{
"Rendering home timeline" => fn -> "Rendering home timeline" => fn ->
StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: home_activities, activities: home_activities,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: !output_relationships
}) })
end, end,
"Rendering direct timeline" => fn -> "Rendering direct timeline" => fn ->
StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: direct_activities, activities: direct_activities,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: !output_relationships
}) })
end, end,
"Rendering public timeline" => fn -> "Rendering public timeline" => fn ->
StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: public_activities, activities: public_activities,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: !output_relationships
}) })
end, end,
"Rendering tag timeline" => fn -> "Rendering tag timeline" => fn ->
StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: tag_activities, activities: tag_activities,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: !output_relationships
}) })
end, end,
"Rendering notifications" => fn -> "Rendering notifications" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{ Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: notifications, notifications: notifications,
for: user for: user,
skip_relationships: !output_relationships
}) })
end, end,
"Rendering favourites timeline" => fn -> "Rendering favourites timeline" => fn ->
StatusView.render("index.json", %{ StatusView.render("index.json", %{
activities: favourites, activities: favourites,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: !output_relationships
}) })
end end
}, },

View file

@ -240,6 +240,8 @@
extended_nickname_format: true, extended_nickname_format: true,
cleanup_attachments: false cleanup_attachments: false
config :pleroma, :extensions, output_relationships_in_statuses_by_default: true
config :pleroma, :feed, config :pleroma, :feed,
post_title: %{ post_title: %{
max_length: 100, max_length: 100,

6
coveralls.json Normal file
View file

@ -0,0 +1,6 @@
{
"skip_files": [
"test/support",
"lib/mix/tasks/pleroma/benchmark.ex"
]
}

View file

@ -392,6 +392,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `email` - `email`
- `name`, optional - `name`, optional
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request, JSON:
```json
[
{
"error": "Appropriate error message here"
}
]
```
## `GET /api/pleroma/admin/users/:nickname/password_reset` ## `GET /api/pleroma/admin/users/:nickname/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname

View file

@ -431,7 +431,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
# Emoji Reactions # Emoji Reactions
Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character. Emoji reactions work a lot like favourites do. They make it possible to react to a post with a single emoji character. To detect the presence of this feature, you can check `pleroma_emoji_reactions` entry in the features list of nodeinfo.
## `PUT /api/v1/pleroma/statuses/:id/reactions/:emoji` ## `PUT /api/v1/pleroma/statuses/:id/reactions/:emoji`
### React to a post with a unicode emoji ### React to a post with a unicode emoji

View file

@ -41,6 +41,6 @@ mix pleroma.emoji gen-pack PACK-URL
Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you. Currently, only .zip archives are recognized as remote pack files and packs are therefore assumed to be zip archives. This command is intended to run interactively and will first ask you some basic questions about the pack, then download the remote file and generate an SHA256 checksum for it, then generate an emoji file list for you.
The manifest entry will either be written to a newly created `index.json` file or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously. The manifest entry will either be written to a newly created `pack_name.json` file (pack name is asked in questions) or appended to the existing one, *replacing* the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted). The file list will be written to the file specified previously, *replacing* that file. You _should_ check that the file list doesn't contain anything you don't need in the pack, that is, anything that is not an emoji (the whole pack is downloaded, but only emoji files are extracted).

View file

@ -5,7 +5,6 @@
defmodule Mix.Pleroma do defmodule Mix.Pleroma do
@doc "Common functions to be reused in mix tasks" @doc "Common functions to be reused in mix tasks"
def start_pleroma do def start_pleroma do
Mix.Task.run("app.start")
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do if Pleroma.Config.get(:env) != :test do

View file

@ -67,7 +67,8 @@ def run(["render_timeline", nickname | _] = args) do
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{ Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: activities, activities: activities,
for: user, for: user,
as: :activity as: :activity,
skip_relationships: true
}) })
end end
}, },

View file

@ -14,8 +14,8 @@ def run(["ls-packs" | args]) do
{options, [], []} = parse_global_opts(args) {options, [], []} = parse_global_opts(args)
manifest = url_or_path = options[:manifest] || default_manifest()
fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest()) manifest = fetch_manifest(url_or_path)
Enum.each(manifest, fn {name, info} -> Enum.each(manifest, fn {name, info} ->
to_print = [ to_print = [
@ -40,9 +40,9 @@ def run(["get-packs" | args]) do
{options, pack_names, []} = parse_global_opts(args) {options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest() url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(manifest_url) manifest = fetch_manifest(url_or_path)
for pack_name <- pack_names do for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do if Map.has_key?(manifest, pack_name) do
@ -75,7 +75,10 @@ def run(["get-packs" | args]) do
end end
# The url specified in files should be in the same directory # The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"]) files_url =
url_or_path
|> Path.dirname()
|> Path.join(pack["files"])
IO.puts( IO.puts(
IO.ANSI.format([ IO.ANSI.format([
@ -133,38 +136,51 @@ def run(["get-packs" | args]) do
end end
end end
def run(["gen-pack", src]) do def run(["gen-pack" | args]) do
start_pleroma() start_pleroma()
proposed_name = Path.basename(src) |> Path.rootname() {opts, [src], []} =
name = String.trim(IO.gets("Pack name [#{proposed_name}]: ")) OptionParser.parse(
# If there's no name, use the default one args,
name = if String.length(name) > 0, do: name, else: proposed_name strict: [
name: :string,
license = String.trim(IO.gets("License: ")) license: :string,
homepage = String.trim(IO.gets("Homepage: ")) homepage: :string,
description = String.trim(IO.gets("Description: ")) description: :string,
files: :string,
proposed_files_name = "#{name}.json" extensions: :string
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: ")) ]
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")
exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
) )
proposed_name = Path.basename(src) |> Path.rootname()
name = get_option(opts, :name, "Pack name:", proposed_name)
license = get_option(opts, :license, "License:")
homepage = get_option(opts, :homepage, "Homepage:")
description = get_option(opts, :description, "Description:")
proposed_files_name = "#{name}_files.json"
files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)
default_exts = [".png", ".gif"]
custom_exts =
get_option(
opts,
:extensions,
"Emoji file extensions (separated with spaces):",
Enum.join(default_exts, " ")
)
|> String.split(" ", trim: true)
exts = exts =
if String.length(exts) > 0 do if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts default_exts
else
custom_exts
end end
IO.puts("Using #{Enum.join(exts, " ")} extensions")
IO.puts("Downloading the pack and generating SHA256") IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(client(), src).body binary_archive = Tesla.get!(client(), src).body
@ -194,14 +210,16 @@ def run(["gen-pack", src]) do
IO.puts(""" IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack. #{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed. Please review the files in the pack and remove those not needed.
""") """)
if File.exists?("index.json") do pack_file = "#{name}.json"
existing_data = File.read!("index.json") |> Jason.decode!()
if File.exists?(pack_file) do
existing_data = File.read!(pack_file) |> Jason.decode!()
File.write!( File.write!(
"index.json", pack_file,
Jason.encode!( Jason.encode!(
Map.merge( Map.merge(
existing_data, existing_data,
@ -211,11 +229,11 @@ def run(["gen-pack", src]) do
) )
) )
IO.puts("index.json file has been update with the #{name} pack") IO.puts("#{pack_file} has been updated with the #{name} pack")
else else
File.write!("index.json", Jason.encode!(pack_json, pretty: true)) File.write!(pack_file, Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack") IO.puts("#{pack_file} has been created with the #{name} pack")
end end
end end

View file

@ -54,10 +54,19 @@ def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do
[:pleroma, nil, :prometheus] [:pleroma, nil, :prometheus]
end end
{logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&transform_and_merge/1)
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
logger
|> Enum.sort()
|> Enum.each(&configure/1)
started_applications = Application.started_applications() started_applications = Application.started_applications()
(Repo.all(ConfigDB) ++ deleted_settings) other
|> Enum.map(&merge_and_update/1) |> Enum.map(&update/1)
|> Enum.uniq() |> Enum.uniq()
|> Enum.reject(&(&1 in reject_restart)) |> Enum.reject(&(&1 in reject_restart))
|> maybe_set_pleroma_last() |> maybe_set_pleroma_last()
@ -81,51 +90,66 @@ defp maybe_set_pleroma_last(apps) do
end end
end end
defp group_for_restart(:logger, key, _, merged_value) do defp transform_and_merge(%{group: group, key: key, value: value} = setting) do
# change logger configuration in runtime, without restart group = ConfigDB.from_string(group)
if Keyword.keyword?(merged_value) and key = ConfigDB.from_string(key)
key not in [:compile_time_application, :backends, :compile_time_purge_matching] do value = ConfigDB.from_binary(value)
Logger.configure_backend(key, merged_value)
else
Logger.configure([{key, merged_value}])
end
nil default = Config.Holder.default_config(group, key)
merged =
cond do
Ecto.get_meta(setting, :state) == :deleted -> default
can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
true -> value
end
{group, key, value, merged}
end end
defp group_for_restart(group, _, _, _) when group != :pleroma, do: group # change logger configuration in runtime, without restart
defp configure({:quack, key, _, merged}) do
defp group_for_restart(group, key, value, _) do Logger.configure_backend(Quack.Logger, [{key, merged}])
if pleroma_need_restart?(group, key, value), do: group :ok = update_env(:quack, key, merged)
end end
defp merge_and_update(setting) do defp configure({_, :backends, _, merged}) do
# removing current backends
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
Enum.each(merged, &Logger.add_backend/1)
:ok = update_env(:logger, :backends, merged)
end
defp configure({group, key, _, merged}) do
merged =
if key == :console do
put_in(merged[:format], merged[:format] <> "\n")
else
merged
end
backend =
if key == :ex_syslogger,
do: {ExSyslogger, :ex_syslogger},
else: key
Logger.configure_backend(backend, merged)
:ok = update_env(:logger, group, merged)
end
defp update({group, key, value, merged}) do
try do try do
key = ConfigDB.from_string(setting.key) :ok = update_env(group, key, merged)
group = ConfigDB.from_string(setting.group)
default = Config.Holder.default_config(group, key) if group != :pleroma or pleroma_need_restart?(group, key, value), do: group
value = ConfigDB.from_binary(setting.value)
merged_value =
cond do
Ecto.get_meta(setting, :state) == :deleted -> default
can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
true -> value
end
:ok = update_env(group, key, merged_value)
group_for_restart(group, key, value, merged_value)
rescue rescue
error -> error ->
error_msg = error_msg =
"updating env causes error, group: " <> "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
inspect(setting.group) <> inspect(value)
" key: " <> } error: #{inspect(error)}"
inspect(setting.key) <>
" value: " <>
inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
Logger.warn(error_msg) Logger.warn(error_msg)
@ -133,6 +157,9 @@ defp merge_and_update(setting) do
end end
end end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
@spec pleroma_need_restart?(atom(), atom(), any()) :: boolean() @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
def pleroma_need_restart?(group, key, value) do def pleroma_need_restart?(group, key, value) do
group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value) group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
@ -150,9 +177,6 @@ defp group_and_subkey_need_reboot?(group, key, value) do
end) end)
end end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env) defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
defp restart(started_applications, app, _) do defp restart(started_applications, app, _) do

View file

@ -4,10 +4,16 @@
import EctoEnum import EctoEnum
defenum(UserRelationshipTypeEnum, defenum(Pleroma.UserRelationship.Type,
block: 1, block: 1,
mute: 2, mute: 2,
reblog_mute: 3, reblog_mute: 3,
notification_mute: 4, notification_mute: 4,
inverse_subscription: 5 inverse_subscription: 5
) )
defenum(Pleroma.FollowingRelationship.State,
follow_pending: 1,
follow_accept: 2,
follow_reject: 3
)

View file

@ -8,12 +8,13 @@ defmodule Pleroma.FollowingRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType alias FlakeId.Ecto.CompatType
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
schema "following_relationships" do schema "following_relationships" do
field(:state, :string, default: "accept") field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType) belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType) belongs_to(:following, User, type: CompatType)
@ -27,6 +28,18 @@ def changeset(%__MODULE__{} = following_relationship, attrs) do
|> put_assoc(:follower, attrs.follower) |> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following) |> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following]) |> validate_required([:state, :follower, :following])
|> unique_constraint(:follower_id,
name: :following_relationships_follower_id_following_id_index
)
|> validate_not_self_relationship()
end
def state_to_enum(state) when state in ["pending", "accept", "reject"] do
String.to_existing_atom("follow_#{state}")
end
def state_to_enum(state) do
raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
end end
def get(%User{} = follower, %User{} = following) do def get(%User{} = follower, %User{} = following) do
@ -35,7 +48,7 @@ def get(%User{} = follower, %User{} = following) do
|> Repo.one() |> Repo.one()
end end
def update(follower, following, "reject"), do: unfollow(follower, following) def update(follower, following, :follow_reject), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do case get(follower, following) do
@ -50,7 +63,7 @@ def update(%User{} = follower, %User{} = following, state) do
end end
end end
def follow(%User{} = follower, %User{} = following, state \\ "accept") do def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
%__MODULE__{} %__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state}) |> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing) |> Repo.insert(on_conflict: :nothing)
@ -80,7 +93,7 @@ def following_count(%User{} = user) do
def get_follow_requests(%User{id: id}) do def get_follow_requests(%User{id: id}) do
__MODULE__ __MODULE__
|> join(:inner, [r], f in assoc(r, :follower)) |> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == "pending") |> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id) |> where([r], r.following_id == ^id)
|> select([r, f], f) |> select([r, f], f)
|> Repo.all() |> Repo.all()
@ -88,7 +101,7 @@ def get_follow_requests(%User{id: id}) do
def following?(%User{id: follower_id}, %User{id: followed_id}) do def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__ __MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept") |> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
|> Repo.exists?() |> Repo.exists?()
end end
@ -97,7 +110,7 @@ def following(%User{} = user) do
__MODULE__ __MODULE__
|> join(:inner, [r], u in User, on: r.following_id == u.id) |> join(:inner, [r], u in User, on: r.following_id == u.id)
|> where([r], r.follower_id == ^user.id) |> where([r], r.follower_id == ^user.id)
|> where([r], r.state == "accept") |> where([r], r.state == ^:follow_accept)
|> select([r, u], u.follower_address) |> select([r, u], u.follower_address)
|> Repo.all() |> Repo.all()
@ -157,4 +170,30 @@ def find(following_relationships, follower, following) do
fr -> fr.follower_id == follower.id and fr.following_id == following.id fr -> fr.follower_id == follower.id and fr.following_id == following.id
end) end)
end end
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
|> validate_following_id_follower_id_inequality()
end
defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :follower_id, fn _, follower_id ->
if follower_id == get_field(changeset, :following_id) do
[source_id: "can't be equal to following_id"]
else
[]
end
end)
end
defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :following_id, fn _, following_id ->
if following_id == get_field(changeset, :follower_id) do
[target_id: "can't be equal to follower_id"]
else
[]
end
end)
end
end end

View file

@ -35,9 +35,19 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
nickname_text = get_nickname_text(nickname, opts) nickname_text = get_nickname_text(nickname, opts)
link = link =
~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{ Phoenix.HTML.Tag.content_tag(
nickname_text :span,
}</span></a></span>) Phoenix.HTML.Tag.content_tag(
:a,
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
href: ap_id,
rel: "ugc"
),
class: "h-card"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@ -49,7 +59,15 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag) tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}" url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
link =
Phoenix.HTML.Tag.content_tag(:a, tag_text,
class: "hashtag",
"data-tag": tag,
href: url,
rel: "tag ugc"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}} {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end end

View file

@ -49,8 +49,10 @@ def open(%URI{} = uri, name, opts) do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}" key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
max_connections = pool_opts[:max_connections] || 250
conn_pid = conn_pid =
if Connections.count(name) < opts[:max_connection] do if Connections.count(name) < max_connections do
do_open(uri, opts) do_open(uri, opts)
else else
close_least_used_and_do_open(name, uri, opts) close_least_used_and_do_open(name, uri, opts)

View file

@ -32,6 +32,18 @@ def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor)
get_actor(%{"actor" => actor}) get_actor(%{"actor" => actor})
end end
def get_object(%{"object" => id}) when is_binary(id) do
id
end
def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
id
end
def get_object(_) do
nil
end
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus # TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
# objects being present in the test suite environment. Once these objects are # objects being present in the test suite environment. Once these objects are
# removed, please also remove this. # removed, please also remove this.

View file

@ -42,13 +42,13 @@ def call(%{assigns: %{valid_signature: true}, params: %{"actor" => actor}} = con
else else
{:user_match, false} -> {:user_match, false} ->
Logger.debug("Failed to map identity from signature (payload actor mismatch)") Logger.debug("Failed to map identity from signature (payload actor mismatch)")
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
assign(conn, :valid_signature, false) assign(conn, :valid_signature, false)
# remove me once testsuite uses mapped capabilities instead of what we do now # remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} -> {:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{key_id_from_conn(conn)}, actor=#{actor}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn conn
end end
end end
@ -60,7 +60,7 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
else else
_ -> _ ->
Logger.debug("Failed to map identity from signature (no payload actor mismatch)") Logger.debug("Failed to map identity from signature (no payload actor mismatch)")
Logger.debug("key_id=#{key_id_from_conn(conn)}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}")
assign(conn, :valid_signature, false) assign(conn, :valid_signature, false)
end end
end end

View file

@ -110,20 +110,9 @@ defp handle(conn, action_settings) do
end end
def disabled?(conn) do def disabled?(conn) do
localhost_or_socket = if Map.has_key?(conn.assigns, :remote_ip_found),
case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do do: !conn.assigns.remote_ip_found,
{127, 0, 0, 1} -> true else: false
{0, 0, 0, 0, 0, 0, 0, 1} -> true
{:local, _} -> true
_ -> false
end
remote_ip_not_found =
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
localhost_or_socket and remote_ip_not_found
end end
@inspect_bucket_not_found {:error, :not_found} @inspect_bucket_not_found {:error, :not_found}

View file

@ -7,8 +7,6 @@ defmodule Pleroma.Plugs.RemoteIp do
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration. This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
""" """
import Plug.Conn
@behaviour Plug @behaviour Plug
@headers ~w[ @headers ~w[
@ -28,12 +26,11 @@ defmodule Pleroma.Plugs.RemoteIp do
def init(_), do: nil def init(_), do: nil
def call(%{remote_ip: original_remote_ip} = conn, _) do def call(conn, _) do
config = Pleroma.Config.get(__MODULE__, []) config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do if Keyword.get(config, :enabled, false) do
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config)) RemoteIp.call(conn, remote_ip_opts(config))
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
else else
conn conn
end end

View file

@ -41,6 +41,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn -> conn ->
conn conn
end end
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
config = Pleroma.Config.get(Pleroma.Upload) config = Pleroma.Config.get(Pleroma.Upload)

View file

@ -243,7 +243,7 @@ def handle_info({:gun_down, conn_pid, _protocol, _reason, _killed}, state) do
@impl true @impl true
def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do
Logger.debug("received DOWM message for #{inspect(conn_pid)} reason -> #{inspect(reason)}") Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
state = state =
with {key, conn} <- find_conn(state.conns, conn_pid) do with {key, conn} <- find_conn(state.conns, conn_pid) do

View file

@ -16,6 +16,7 @@ defmodule Pleroma.User do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Delivery alias Pleroma.Delivery
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Keys alias Pleroma.Keys
alias Pleroma.Notification alias Pleroma.Notification
@ -452,7 +453,7 @@ defp put_fields(changeset) do
fields = fields =
raw_fields raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
changeset changeset
|> put_change(:raw_fields, raw_fields) |> put_change(:raw_fields, raw_fields)
@ -462,6 +463,12 @@ defp put_fields(changeset) do
end end
end end
defp parse_fields(value) do
value
|> Formatter.linkify(mentions_format: :full)
|> elem(0)
end
defp put_change_if_present(changeset, map_field, value_function) do defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do with {:ok, new_value} <- value_function.(value) do
@ -693,7 +700,7 @@ def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, "pending") follow(follower, followed, :follow_pending)
end end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@ -713,14 +720,14 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
def follow_all(follower, followeds) do def follow_all(follower, followeds) do
followeds followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end) |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, "accept")) |> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower) set_cache(follower)
end end
defdelegate following(user), to: FollowingRelationship defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ "accept") do def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
cond do cond do
@ -747,7 +754,7 @@ def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
def unfollow(%User{} = follower, %User{} = followed) do def unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do case get_follow_state(follower, followed) do
state when state in ["accept", "pending"] -> state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed) FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -765,6 +772,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
defdelegate following?(follower, followed), to: FollowingRelationship defdelegate following?(follower, followed), to: FollowingRelationship
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following) following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship) get_follow_state(follower, following, following_relationship)
@ -778,8 +786,11 @@ def get_follow_state(
case {following_relationship, following.local} do case {following_relationship, following.local} do
{nil, false} -> {nil, false} ->
case Utils.fetch_latest_follow(follower, following) do case Utils.fetch_latest_follow(follower, following) do
%{data: %{"state" => state}} when state in ["pending", "accept"] -> state %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
_ -> nil FollowingRelationship.state_to_enum(state)
_ ->
nil
end end
{%{state: state}, _} -> {%{state: state}, _} ->
@ -1278,7 +1289,7 @@ def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) || blocks_user?(user, target) ||
(!User.following?(user, target) && blocks_domain?(user, target)) (blocks_domain?(user, target) and not User.following?(user, target))
end end
def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(%User{} = user, %User{} = target) do
@ -1979,17 +1990,6 @@ def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields def fields(%{fields: fields}), do: fields
def sanitized_fields(%User{} = user) do
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
end
def validate_fields(changeset, remote? \\ false) do def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0) limit = Pleroma.Config.get([:instance, limit_name], 0)

View file

@ -148,7 +148,7 @@ defp compose_query({:followers, %User{id: id}}, query) do
as: :relationships, as: :relationships,
on: r.following_id == ^id and r.follower_id == u.id on: r.following_id == ^id and r.follower_id == u.id
) )
|> where([relationships: r], r.state == "accept") |> where([relationships: r], r.state == ^:follow_accept)
end end
defp compose_query({:friends, %User{id: id}}, query) do defp compose_query({:friends, %User{id: id}}, query) do
@ -158,7 +158,7 @@ defp compose_query({:friends, %User{id: id}}, query) do
as: :relationships, as: :relationships,
on: r.following_id == u.id and r.follower_id == ^id on: r.following_id == u.id and r.follower_id == ^id
) )
|> where([relationships: r], r.state == "accept") |> where([relationships: r], r.state == ^:follow_accept)
end end
defp compose_query({:recipients_from_activity, to}, query) do defp compose_query({:recipients_from_activity, to}, query) do
@ -173,7 +173,7 @@ defp compose_query({:recipients_from_activity, to}, query) do
) )
|> where( |> where(
[u, following: f, relationships: r], [u, following: f, relationships: r],
u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept") u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
) )
|> distinct(true) |> distinct(true)
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Ecto.Changeset
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -16,12 +17,12 @@ defmodule Pleroma.UserRelationship do
schema "user_relationships" do schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType) belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType) belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, UserRelationshipTypeEnum) field(:relationship_type, Pleroma.UserRelationship.Type)
timestamps(updated_at: false) timestamps(updated_at: false)
end end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do
# `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`, # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
# `def create_notification_mute/2`, `def create_inverse_subscription/2` # `def create_notification_mute/2`, `def create_inverse_subscription/2`
def unquote(:"create_#{relationship_type}")(source, target), def unquote(:"create_#{relationship_type}")(source, target),
@ -40,7 +41,7 @@ def unquote(:"#{relationship_type}_exists?")(source, target),
def user_relationship_types, do: Keyword.keys(user_relationship_mappings()) def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__() def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship user_relationship
@ -129,17 +130,27 @@ def exists?(dictionary, rel_type, source, target, func) do
end end
@doc ":relationships option for StatusView / AccountView / NotificationView" @doc ":relationships option for StatusView / AccountView / NotificationView"
def view_relationships_option(nil = _reading_user, _actors) do def view_relationships_option(reading_user, actors, opts \\ [])
def view_relationships_option(nil = _reading_user, _actors, _opts) do
%{user_relationships: [], following_relationships: []} %{user_relationships: [], following_relationships: []}
end end
def view_relationships_option(%User{} = reading_user, actors) do def view_relationships_option(%User{} = reading_user, actors, opts) do
{source_to_target_rel_types, target_to_source_rel_types} =
if opts[:source_mutes_only] do
# This option is used for rendering statuses (FE needs `muted` flag for each one anyways)
{[:mute], []}
else
{[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
end
user_relationships = user_relationships =
UserRelationship.dictionary( UserRelationship.dictionary(
[reading_user], [reading_user],
actors, actors,
[:block, :mute, :notification_mute, :reblog_mute], source_to_target_rel_types,
[:block, :inverse_subscription] target_to_source_rel_types
) )
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors) following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
@ -147,16 +158,14 @@ def view_relationships_option(%User{} = reading_user, actors) do
%{user_relationships: user_relationships, following_relationships: following_relationships} %{user_relationships: user_relationships, following_relationships: following_relationships}
end end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset changeset
|> validate_change(:target_id, fn _, target_id -> |> validate_source_id_target_id_inequality()
if target_id == get_field(changeset, :source_id) do |> validate_target_id_source_id_inequality()
[target_id: "can't be equal to source_id"] end
else
[] defp validate_source_id_target_id_inequality(%Changeset{} = changeset) do
end validate_change(changeset, :source_id, fn _, source_id ->
end)
|> validate_change(:source_id, fn _, source_id ->
if source_id == get_field(changeset, :target_id) do if source_id == get_field(changeset, :target_id) do
[source_id: "can't be equal to target_id"] [source_id: "can't be equal to target_id"]
else else
@ -164,4 +173,14 @@ defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
end end
end) end)
end end
defp validate_target_id_source_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :target_id, fn _, target_id ->
if target_id == get_field(changeset, :source_id) do
[target_id: "can't be equal to source_id"]
else
[]
end
end)
end
end end

View file

@ -125,6 +125,21 @@ def increase_poll_votes_if_vote(%{
def increase_poll_votes_if_vote(_create_data), do: :noop def increase_poll_votes_if_vote(_create_data), do: :noop
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
{:ok, activity} <-
Repo.insert(%Activity{
data: object,
local: local,
recipients: recipients,
actor: object["actor"]
}) do
{:ok, activity, meta}
end
end
@spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()} @spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map), with nil <- Activity.normalize(map),
@ -706,7 +721,7 @@ def move(%User{} = origin, %User{} = target, local \\ true) do
end end
end end
defp fetch_activities_for_context_query(context, opts) do def fetch_activities_for_context_query(context, opts) do
public = [Constants.as_public()] public = [Constants.as_public()]
recipients = recipients =

View file

@ -0,0 +1,43 @@
defmodule Pleroma.Web.ActivityPub.Builder do
@moduledoc """
This module builds the objects. Meant to be used for creating local objects.
This module encodes our addressing policies and general shape of our objects.
"""
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
# Address the actor of the object, and our actor's follower collection if the post is public.
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
# CC everyone who's been addressed in the object, except ourself and the object actor's
# follower collection
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => "Like",
"object" => object.data["id"],
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}, []}
end
end

View file

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@moduledoc """
This module is responsible for validating an object (which can be an activity)
and checking if it is both well formed and also compatible with our view of
the system.
"""
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
{:ok, object, meta}
end
end
def stringify_keys(object) do
object
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
def fetch_actor_and_object(object) do
User.get_or_fetch_by_ap_id(object["actor"])
Object.normalize(object["object"])
:ok
end
end

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
import Ecto.Changeset
alias Pleroma.Object
alias Pleroma.User
def validate_actor_presence(cng, field_name \\ :actor) do
cng
|> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do
[]
else
[{field_name, "can't find user"}]
end
end)
end
def validate_object_presence(cng, field_name \\ :object) do
cng
|> validate_change(field_name, fn field_name, object ->
if Object.get_cached_by_ap_id(object) do
[]
else
[{field_name, "can't find object"}]
end
end)
end
end

View file

@ -0,0 +1,30 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:actor, Types.ObjectID)
field(:type, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
embeds_one(:object, NoteValidator)
end
def cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end
end

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_existing_like()
end
def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
cng
|> add_error(:actor, "already liked this object")
|> add_error(:object, "already liked by this actor")
else
cng
end
end
def validate_existing_like(cng), do: cng
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
field(:type, :string)
field(:content, :string)
field(:context, :string)
field(:actor, Types.ObjectID)
field(:attributedTo, Types.ObjectID)
field(:summary, :string)
field(:published, Types.DateTime)
# TODO: Write type
field(:emoji, :map, default: %{})
field(:sensitive, :boolean, default: false)
# TODO: Write type
field(:attachment, {:array, :map}, default: [])
field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
# see if needed
field(:conversation, :string)
field(:context_id, :string)
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Note"])
|> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
end
end

View file

@ -0,0 +1,34 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
@moduledoc """
The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
DateTime can't parse this, but it can parse the related iso8601. This
module punches the date until it looks like iso8601 and normalizes to
it.
DateTimes without a timezone offset are treated as UTC.
Reference: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published
"""
use Ecto.Type
def type, do: :string
def cast(datetime) when is_binary(datetime) do
with {:ok, datetime, _} <- DateTime.from_iso8601(datetime) do
{:ok, DateTime.to_iso8601(datetime)}
else
{:error, :missing_offset} -> cast("#{datetime}Z")
_e -> :error
end
end
def cast(_), do: :error
def dump(data) do
{:ok, data}
end
def load(data) do
{:ok, data}
end
end

View file

@ -0,0 +1,29 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
use Ecto.Type
def type, do: :string
def cast(object) when is_binary(object) do
# Host has to be present and scheme has to be an http scheme (for now)
case URI.parse(object) do
%URI{host: nil} -> :error
%URI{host: ""} -> :error
%URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object}
_ -> :error
end
end
def cast(%{"id" => object}), do: cast(object)
def cast(_) do
:error
end
def dump(data) do
{:ok, data}
end
def load(data) do
{:ok, data}
end
end

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
{_, {:ok, %Activity{} = activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, %Activity{} = activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
{:mrf_object, {:reject, _}} -> {:ok, nil, meta}
e -> {:error, e}
end
end
defp maybe_federate(activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do
Federator.publish(activity)
{:ok, :federated}
else
{:ok, :not_federated}
end
else
_e -> {:error, :badarg}
end
end
end

View file

@ -0,0 +1,28 @@
defmodule Pleroma.Web.ActivityPub.SideEffects do
@moduledoc """
This module looks at an inserted object and executes the side effects that it
implies. For example, a `Like` activity will increase the like count on the
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
# Tasks this handles:
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object)
{:ok, object, meta}
end
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
end
end

View file

@ -13,6 +13,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@ -202,16 +205,46 @@ def fix_context(object) do
|> Map.put("conversation", context) |> Map.put("conversation", context)
end end
defp add_if_present(map, _key, nil), do: map
defp add_if_present(map, key, value) do
Map.put(map, key, value)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments = attachments =
Enum.map(attachment, fn data -> Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"] url =
href = data["url"] || data["href"] cond do
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] is_list(data["url"]) -> List.first(data["url"])
is_map(data["url"]) -> data["url"]
true -> nil
end
data media_type =
|> Map.put("mediaType", media_type) cond do
|> Map.put("url", url) is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
is_binary(data["mediaType"]) -> data["mediaType"]
is_binary(data["mimeType"]) -> data["mimeType"]
true -> nil
end
href =
cond do
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
end
attachment_url =
%{"href" => href}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", data["type"])
|> add_if_present("name", data["name"])
end) end)
Map.put(object, "attachment", attachments) Map.put(object, "attachment", attachments)
@ -491,7 +524,8 @@ def handle_incoming(
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <- {_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")}, {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do {:ok, _relationship} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
actor: followed, actor: followed,
@ -501,7 +535,7 @@ def handle_incoming(
else else
{:user_blocked, true} -> {:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -512,7 +546,7 @@ def handle_incoming(
{:follow, {:error, _}} -> {:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject") {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -522,7 +556,7 @@ def handle_incoming(
}) })
{:user_locked, true} -> {:user_locked, true} ->
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending") {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
:noop :noop
end end
@ -542,7 +576,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{ ActivityPub.accept(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
type: "Accept", type: "Accept",
@ -565,7 +599,7 @@ def handle_incoming(
{:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, activity} <- {:ok, activity} <-
ActivityPub.reject(%{ ActivityPub.reject(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
@ -609,17 +643,20 @@ def handle_incoming(
|> handle_incoming(options) |> handle_incoming(options)
end end
def handle_incoming( def handle_incoming(%{"type" => "Like"} = data, _options) do
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data, with {_, {:ok, cast_data_sym}} <-
_options {:casting_data,
) do data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
with actor <- Containment.get_actor(data), cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), :ok <- ObjectValidator.fetch_actor_and_object(cast_data),
{:ok, object} <- get_obj_helper(object_id), {_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do {_, {:ok, cast_data}} <-
{:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
{_, {:ok, activity, _meta}} <-
{:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
{:ok, activity} {:ok, activity}
else else
_e -> :error e -> {:error, e}
end end
end end
@ -1243,4 +1280,45 @@ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
def maybe_fix_user_url(data), do: data def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data) def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
do: {:ok, data}
defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
{:ok, Map.put(data, "context", context)}
else
_ ->
{:error, :no_context}
end
end
defp ensure_context_presence(_) do
{:error, :no_context}
end
defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
do: {:ok, data}
defp ensure_recipients_presence(%{"object" => object} = data) do
case Object.normalize(object) do
%{data: %{"actor" => actor}} ->
data =
data
|> Map.put("to", [actor])
|> Map.put("cc", data["cc"] || [])
{:ok, data}
nil ->
{:error, :no_object}
_ ->
{:error, :no_actor}
end
end
defp ensure_recipients_presence(_) do
{:error, :no_object}
end
end end

View file

@ -258,7 +258,7 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
conn conn
|> put_view(Pleroma.Web.AdminAPI.StatusView) |> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity}) |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do def list_user_statuses(conn, %{"nickname" => nickname} = params) do
@ -277,7 +277,7 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity}) |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -576,9 +576,8 @@ def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target})
@doc "Sends registration invite via email" @doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with true <- with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
Config.get([:instance, :invites_enabled]) && {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
!Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- UserInviteToken.create_invite(), {:ok, invite_token} <- UserInviteToken.create_invite(),
email <- email <-
Pleroma.Emails.UserEmail.user_invitation_email( Pleroma.Emails.UserEmail.user_invitation_email(
@ -589,6 +588,18 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params)
), ),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "") json_response(conn, :no_content, "")
else
{:registrations_open, _} ->
errors(
conn,
{:error, "To send invites you need to set the `registrations_open` option to false."}
)
{:invites_enabled, _} ->
errors(
conn,
{:error, "To send invites you need to set the `invites_enabled` option to true."}
)
end end
end end
@ -801,7 +812,7 @@ def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
conn conn
|> put_view(Pleroma.Web.AdminAPI.StatusView) |> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity}) |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do

View file

@ -38,7 +38,12 @@ def render("show.json", %{report: report, user: user, account: account, statuses
actor: merge_account_views(user), actor: merge_account_views(user),
content: content, content: content,
created_at: created_at, created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), statuses:
StatusView.render("index.json", %{
activities: statuses,
as: :activity,
skip_relationships: false
}),
state: report.data["state"], state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}) notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
} }

View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec do
alias OpenApiSpex.OpenApi
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
OpenApiSpex.Server.from_endpoint(Endpoint)
],
info: %OpenApiSpex.Info{
title: "Pleroma",
description: Application.spec(:pleroma, :description) |> to_string(),
version: Application.spec(:pleroma, :vsn) |> to_string()
},
# populate the paths from a phoenix router
paths: OpenApiSpex.Paths.from_router(Router),
components: %OpenApiSpex.Components{
securitySchemes: %{
"oAuth" => %OpenApiSpex.SecurityScheme{
type: "oauth2",
flows: %OpenApiSpex.OAuthFlows{
password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
}
}
}
}
}
}
# discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Helpers do
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
content =
media_types
|> Enum.map(fn type ->
{type,
%OpenApiSpex.MediaType{
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}}
end)
|> Enum.into(%{})
%OpenApiSpex.RequestBody{
description: description,
content: content,
required: opts[:required] || false
}
end
end

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AppOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
@spec create_operation() :: Operation.t()
def create_operation do
%Operation{
tags: ["apps"],
summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create",
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
responses: %{
200 => Operation.response("App", "application/json", AppCreateResponse),
422 =>
Operation.response(
"Unprocessable Entity",
"application/json",
%Schema{
type: :object,
description:
"If a required parameter is missing or improperly formatted, the request will fail.",
properties: %{
error: %Schema{type: :string}
},
example: %{
"error" => "Validation failed: Redirect URI must be an absolute URI."
}
}
)
}
}
end
def verify_credentials_operation do
%Operation{
tags: ["apps"],
summary: "Verify your app works",
description: "Confirm that the app's OAuth2 credentials work.",
operationId: "AppController.verify_credentials",
security: [
%{
"oAuth" => ["read"]
}
],
responses: %{
200 =>
Operation.response("App", "application/json", %Schema{
type: :object,
description:
"If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
properties: %{
name: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"name" => "My App",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
}),
422 =>
Operation.response(
"Unauthorized",
"application/json",
%Schema{
type: :object,
description:
"If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
properties: %{
error: %Schema{type: :string}
},
example: %{
"error" => "The access token is invalid."
}
}
)
}
}
end
end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Fetch domain blocks",
description: "View domains the user has blocked.",
security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index",
responses: %{
200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse)
}
}
end
def create_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Block a domain",
description: """
Block a domain to:
- hide all public posts from it
- hide all notifications from it
- remove all followers from it
- prevent following new users from it (but does not remove existing follows)
""",
operationId: "DomainBlockController.create",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
def delete_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes. If none is provided, defaults to `read`."
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
})
end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
})
end

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "DomainBlockRequest",
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain],
example: %{
"domain" => "facebook.com"
}
})
end

View file

@ -0,0 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "DomainBlocksResponse",
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
end

View file

@ -187,7 +187,7 @@ defp object(draft) do
end end
defp preview?(draft) do defp preview?(draft) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])
%__MODULE__{draft | preview?: preview?} %__MODULE__{draft | preview?: preview?}
end end

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@ -19,6 +21,7 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants require Pleroma.Constants
require Logger
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -42,7 +45,7 @@ def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed), with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
{:ok, _activity} <- {:ok, _activity} <-
ActivityPub.accept(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -57,7 +60,7 @@ def accept_follow_request(follower, followed) do
def reject_follow_request(follower, followed) do def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, _activity} <- {:ok, _activity} <-
ActivityPub.reject(%{ ActivityPub.reject(%{
to: [follower.ap_id], to: [follower.ap_id],
@ -109,18 +112,51 @@ def unrepeat(id_or_ap_id, user) do
end end
end end
def favorite(id_or_ap_id, user) do @spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, def favorite(%User{} = user, id) do
object <- Object.normalize(activity), case favorite_helper(user, id) do
like_activity <- Utils.get_existing_like(user.ap_id, object) do {:ok, _} = res ->
if like_activity do res
{:ok, like_activity, object}
else {:error, :not_found} = res ->
ActivityPub.like(user, object) res
end
{:error, e} ->
Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not favorite")}
end
end
def favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else else
{:find_activity, _} -> {:error, :not_found} {:find_object, _} ->
_ -> {:error, dgettext("errors", "Could not favorite")} {:error, :not_found}
{:common_pipeline,
{
:error,
{
:validate_object,
{
:error,
changeset
}
}
}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
else
{:error, e}
end
e ->
{:error, e}
end end
end end

View file

@ -5,10 +5,18 @@
defmodule Pleroma.Web.ControllerHelper do defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller use Pleroma.Web, :controller
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html alias Pleroma.Config
# As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"] @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values def explicitly_falsy_param?(value), do: value in @falsy_param_values
# Note: `nil` and `""` are considered falsy values in Pleroma
def falsy_param?(value),
do: explicitly_falsy_param?(value) or value in [nil, ""]
def truthy_param?(value), do: not falsy_param?(value)
def json_response(conn, status, json) do def json_response(conn, status, json) do
conn conn
@ -96,4 +104,14 @@ def try_render(conn, _, _) do
def put_if_exist(map, _key, nil), do: map def put_if_exist(map, _key, nil), do: map
def put_if_exist(map, key, value), do: Map.put(map, key, value) def put_if_exist(map, key, value), do: Map.put(map, key, value)
@doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications"
def skip_relationships?(params) do
if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do
false
else
# BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships.
not truthy_param?(params["with_relationships"])
end
end
end end

View file

@ -6,7 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] only: [
add_link_headers: 2,
truthy_param?: 1,
assign_account_by_id: 2,
json_response: 3,
skip_relationships?: 1
]
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -240,7 +246,12 @@ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", activities: activities, for: reading_user, as: :activity) |> render("index.json",
activities: activities,
for: reading_user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else else
_e -> render_error(conn, :not_found, "Can't find user") _e -> render_error(conn, :not_found, "Can't find user")
end end

View file

@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials) plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
plug(OpenApiSpex.Plug.CastAndValidate)
@local_mastodon_name "Mastodon-Local" @local_mastodon_name "Mastodon-Local"
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
@doc "POST /api/v1/apps" @doc "POST /api/v1/apps"
def create(conn, params) do def create(%{body_params: params} = conn, _params) do
scopes = Scopes.fetch_scopes(params, ["read"]) scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs = app_attrs =
params params
|> Map.drop(["scope", "scopes"]) |> Map.take([:client_name, :redirect_uris, :website])
|> Map.put("scopes", scopes) |> Map.put(:scopes, scopes)
with cs <- App.register_changeset(%App{}, app_attrs), with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name, false <- cs.changes[:client_name] == @local_mastodon_name,

View file

@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User alias Pleroma.User
plug(OpenApiSpex.Plug.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index %{scopes: ["follow", "read:blocks"]} when action == :index
@ -26,13 +29,13 @@ def index(%{assigns: %{user: user}} = conn, _) do
end end
@doc "POST /api/v1/domain_blocks" @doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.block_domain(blocker, domain) User.block_domain(blocker, domain)
json(conn, %{}) json(conn, %{})
end end
@doc "DELETE /api/v1/domain_blocks" @doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.unblock_domain(blocker, domain) User.unblock_domain(blocker, domain)
json(conn, %{}) json(conn, %{})
end end

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.NotificationController do defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -45,7 +45,11 @@ def index(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(notifications) |> add_link_headers(notifications)
|> render("index.json", notifications: notifications, for: user) |> render("index.json",
notifications: notifications,
for: user,
skip_relationships: skip_relationships?(params)
)
end end
# GET /api/v1/notifications/:id # GET /api/v1/notifications/:id
@ -66,7 +70,8 @@ def clear(%{assigns: %{user: user}} = conn, _params) do
json(conn, %{}) json(conn, %{})
end end
# POST /api/v1/notifications/dismiss # POST /api/v1/notifications/:id/dismiss
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{}) json(conn, %{})

View file

@ -5,13 +5,14 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
@ -66,10 +67,11 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
defp search_options(params, user) do defp search_options(params, user) do
[ [
skip_relationships: skip_relationships?(params),
resolve: params["resolve"] == "true", resolve: params["resolve"] == "true",
following: params["following"] == "true", following: params["following"] == "true",
limit: ControllerHelper.fetch_integer_param(params, "limit"), limit: fetch_integer_param(params, "limit"),
offset: ControllerHelper.fetch_integer_param(params, "offset"), offset: fetch_integer_param(params, "offset"),
type: params["type"], type: params["type"],
author: get_author(params), author: get_author(params),
for_user: user for_user: user
@ -79,12 +81,24 @@ defp search_options(params, user) do
defp resource_search(_, "accounts", query, options) do defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end) accounts = with_fallback(fn -> User.search(query, options) end)
AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
AccountView.render("index.json",
users: accounts,
for: options[:for_user],
as: :user,
skip_relationships: false
)
end end
defp resource_search(_, "statuses", query, options) do defp resource_search(_, "statuses", query, options) do
statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity)
StatusView.render("index.json",
activities: statuses,
for: options[:for_user],
as: :activity,
skip_relationships: options[:skip_relationships]
)
end end
defp resource_search(:v2, "hashtags", query, _options) do defp resource_search(:v2, "hashtags", query, _options) do

View file

@ -5,7 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2] import Pleroma.Web.ControllerHelper,
only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]
require Ecto.Query require Ecto.Query
@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
`ids` query param is required `ids` query param is required
""" """
def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
limit = 100 limit = 100
activities = activities =
@ -110,7 +111,12 @@ def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
|> Activity.all_by_ids_with_object() |> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user)) |> Enum.filter(&Visibility.visible_for_user?(&1, user))
render(conn, "index.json", activities: activities, for: user, as: :activity) render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
@doc """ @doc """
@ -207,9 +213,9 @@ def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
end end
@doc "POST /api/v1/statuses/:id/favourite" @doc "POST /api/v1/statuses/:id/favourite"
def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end end
end end
@ -360,7 +366,12 @@ def favourites(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
@doc "GET /api/v1/bookmarks" @doc "GET /api/v1/bookmarks"
@ -378,6 +389,11 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(bookmarks) |> add_link_headers(bookmarks)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
end end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -14,9 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
# TODO: Replace with a macro when there is a Phoenix release with # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
# in it
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct) plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public) plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
@ -49,7 +48,12 @@ def home(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
# GET /api/v1/timelines/direct # GET /api/v1/timelines/direct
@ -68,7 +72,12 @@ def direct(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
# GET /api/v1/timelines/public # GET /api/v1/timelines/public
@ -95,7 +104,12 @@ def public(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(activities, %{"local" => local_only}) |> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else else
render_error(conn, :unauthorized, "authorization required for timeline view") render_error(conn, :unauthorized, "authorization required for timeline view")
end end
@ -140,7 +154,12 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(activities, %{"local" => local_only}) |> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
# GET /api/v1/timelines/list/:list_id # GET /api/v1/timelines/list/:list_id
@ -164,7 +183,12 @@ def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
|> ActivityPub.fetch_activities_bounded(following, params) |> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse() |> Enum.reverse()
render(conn, "index.json", activities: activities, for: user, as: :activity) render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else else
_e -> render_error(conn, :forbidden, "Error.") _e -> render_error(conn, :forbidden, "Error.")
end end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
def render("index.json", %{users: users} = opts) do def render("index.json", %{users: users} = opts) do
reading_user = opts[:for] reading_user = opts[:for]
# Note: :skip_relationships option is currently intentionally not supported for accounts
relationships_opt = relationships_opt =
cond do cond do
Map.has_key?(opts, :relationships) -> Map.has_key?(opts, :relationships) ->
@ -73,7 +74,7 @@ def render(
followed_by = followed_by =
if following_relationships do if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: "accept"} -> true %{state: :follow_accept} -> true
_ -> false _ -> false
end end
else else
@ -83,7 +84,7 @@ def render(
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{ %{
id: to_string(target.id), id: to_string(target.id),
following: follow_state == "accept", following: follow_state == :follow_accept,
followed_by: followed_by, followed_by: followed_by,
blocking: blocking:
UserRelationship.exists?( UserRelationship.exists?(
@ -125,7 +126,7 @@ def render(
reading_user, reading_user,
&User.subscribed_to?(&2, &1) &User.subscribed_to?(&2, &1)
), ),
requested: follow_state == "pending", requested: follow_state == :follow_pending,
domain_blocking: User.blocks_domain?(reading_user, target), domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs: showing_reblogs:
not UserRelationship.exists?( not UserRelationship.exists?(
@ -192,11 +193,15 @@ defp do_render("show.json", %{user: user} = opts) do
end) end)
relationship = relationship =
render("relationship.json", %{ if opts[:skip_relationships] do
user: opts[:for], %{}
target: user, else
relationships: opts[:relationships] render("relationship.json", %{
}) user: opts[:for],
target: user,
relationships: opts[:relationships]
})
end
%{ %{
id: to_string(user.id), id: to_string(user.id),

View file

@ -51,14 +51,15 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
|> Enum.filter(& &1) |> Enum.filter(& &1)
|> Kernel.++(move_activities_targets) |> Kernel.++(move_activities_targets)
UserRelationship.view_relationships_option(reading_user, actors) UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
)
end end
opts = %{ opts =
for: reading_user, opts
parent_activities: parent_activities, |> Map.put(:parent_activities, parent_activities)
relationships: relationships_opt |> Map.put(:relationships, relationships_opt)
}
safe_render_many(notifications, NotificationView, "show.json", opts) safe_render_many(notifications, NotificationView, "show.json", opts)
end end
@ -82,12 +83,16 @@ def render(
mastodon_type = Activity.mastodon_notification_type(activity) mastodon_type = Activity.mastodon_notification_type(activity)
render_opts = %{
relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}
with %{id: _} = account <- with %{id: _} = account <-
AccountView.render("show.json", %{ AccountView.render(
user: actor, "show.json",
for: reading_user, Map.merge(render_opts, %{user: actor, for: reading_user})
relationships: opts[:relationships] ) do
}) do
response = %{ response = %{
id: to_string(notification.id), id: to_string(notification.id),
type: mastodon_type, type: mastodon_type,
@ -98,8 +103,6 @@ def render(
} }
} }
render_opts = %{relationships: opts[:relationships]}
case mastodon_type do case mastodon_type do
"mention" -> "mention" ->
put_status(response, activity, reading_user, render_opts) put_status(response, activity, reading_user, render_opts)
@ -111,6 +114,7 @@ def render(
put_status(response, parent_activity_fn.(), reading_user, render_opts) put_status(response, parent_activity_fn.(), reading_user, render_opts)
"move" -> "move" ->
# Note: :skip_relationships option being applied to _account_ rendering (here)
put_target(response, activity, reading_user, render_opts) put_target(response, activity, reading_user, render_opts)
"follow" -> "follow" ->

View file

@ -99,7 +99,9 @@ def render("index.json", opts) do
true -> true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
UserRelationship.view_relationships_option(reading_user, actors) UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
)
end end
opts = opts =
@ -153,7 +155,8 @@ def render(
AccountView.render("show.json", %{ AccountView.render("show.json", %{
user: user, user: user,
for: opts[:for], for: opts[:for],
relationships: opts[:relationships] relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}), }),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
@ -301,6 +304,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
_ -> [] _ -> []
end end
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted = muted =
thread_muted? || thread_muted? ||
UserRelationship.exists?( UserRelationship.exists?(
@ -319,7 +323,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
AccountView.render("show.json", %{ AccountView.render("show.json", %{
user: user, user: user,
for: opts[:for], for: opts[:for],
relationships: opts[:relationships] relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}), }),
in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),

View file

@ -75,7 +75,8 @@ def raw_nodeinfo do
end, end,
if Config.get([:instance, :safe_dm_mentions]) do if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions" "safe_dm_mentions"
end end,
"pleroma_emoji_reactions"
] ]
|> Enum.filter(& &1) |> Enum.filter(& &1)

View file

@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do
Note: `scopes` is used by Mastodon supporting it but sticking to Note: `scopes` is used by Mastodon supporting it but sticking to
OAuth's standard `scope` wherever we control it OAuth's standard `scope` wherever we control it
""" """
@spec fetch_scopes(map(), list()) :: list() @spec fetch_scopes(map() | struct(), list()) :: list()
def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
parse_scopes(scopes, default)
end
def fetch_scopes(params, default) do def fetch_scopes(params, default) do
parse_scopes(params["scope"] || params["scopes"], default) parse_scopes(params["scope"] || params["scopes"], default)
end end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -139,7 +139,12 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", activities: activities, for: for_user, as: :activity) |> render("index.json",
activities: activities,
for: for_user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe" @doc "POST /api/v1/pleroma/accounts/:id/subscribe"

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
@ -110,12 +110,11 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id})
end end
def conversation_statuses( def conversation_statuses(
%{assigns: %{user: user}} = conn, %{assigns: %{user: %{id: user_id} = user}} = conn,
%{"id" => participation_id} = params %{"id" => participation_id} = params
) do ) do
with %Participation{} = participation <- with %Participation{user_id: ^user_id} = participation <-
Participation.get(participation_id, preload: [:conversation]), Participation.get(participation_id, preload: [:conversation]) do
true <- user.id == participation.user_id do
params = params =
params params
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
@ -124,13 +123,19 @@ def conversation_statuses(
activities = activities =
participation.conversation.ap_id participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context(params) |> ActivityPub.fetch_activities_for_context_query(params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
|> Enum.reverse() |> Enum.reverse()
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else else
_error -> _error ->
conn conn
@ -184,13 +189,17 @@ def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_i
end end
end end
def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
with notifications <- Notification.set_read_up_to(user, max_id) do with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80) notifications = Enum.take(notifications, 80)
conn conn
|> put_view(NotificationView) |> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user}) |> render("index.json",
notifications: notifications,
for: user,
skip_relationships: skip_relationships?(params)
)
end end
end end
end end

View file

@ -64,5 +64,8 @@ def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) d
def fetch_data_for_activity(_), do: %{} def fetch_data_for_activity(_), do: %{}
def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity) def perform(:fetch, %Activity{} = activity) do
fetch_data_for_activity(activity)
:ok
end
end end

View file

@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Plugs.IdempotencyPlug) plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end end
pipeline :authenticated_api do pipeline :authenticated_api do
@ -45,6 +46,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug) plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end end
pipeline :admin_api do pipeline :admin_api do
@ -62,6 +64,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug) plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug) plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end end
pipeline :mastodon_html do pipeline :mastodon_html do
@ -95,10 +98,12 @@ defmodule Pleroma.Web.Router do
pipeline :config do pipeline :config do
plug(:accepts, ["json", "xml"]) plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end end
pipeline :pleroma_api do pipeline :pleroma_api do
plug(:accepts, ["html", "json"]) plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end end
pipeline :mailbox_preview do pipeline :mailbox_preview do
@ -348,9 +353,11 @@ defmodule Pleroma.Web.Router do
get("/notifications", NotificationController, :index) get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show) get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear) post("/notifications/clear", NotificationController, :clear)
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple) delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss)
get("/scheduled_statuses", ScheduledActivityController, :index) get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show) get("/scheduled_statuses/:id", ScheduledActivityController, :show)
@ -501,6 +508,12 @@ defmodule Pleroma.Web.Router do
) )
end end
scope "/api" do
pipe_through(:api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)

18
mix.exs
View file

@ -37,12 +37,21 @@ def project do
pleroma: [ pleroma: [
include_executables_for: [:unix], include_executables_for: [:unix],
applications: [ex_syslogger: :load, syslog: :load], applications: [ex_syslogger: :load, syslog: :load],
steps: [:assemble, &copy_files/1, &copy_nginx_config/1] steps: [:assemble, &put_otp_version/1, &copy_files/1, &copy_nginx_config/1]
] ]
] ]
] ]
end end
def put_otp_version(%{path: target_path} = release) do
File.write!(
Path.join([target_path, "OTP_VERSION"]),
Pleroma.OTPVersion.version()
)
release
end
def copy_files(%{path: target_path} = release) do def copy_files(%{path: target_path} = release) do
File.cp_r!("./rel/files", target_path) File.cp_r!("./rel/files", target_path)
release release
@ -108,7 +117,7 @@ defp deps do
{:ecto_enum, "~> 1.4"}, {:ecto_enum, "~> 1.4"},
{:ecto_sql, "~> 3.3.2"}, {:ecto_sql, "~> 3.3.2"},
{:postgrex, ">= 0.13.5"}, {:postgrex, ">= 0.13.5"},
{:oban, "~> 0.12.1"}, {:oban, "~> 1.2"},
{:gettext, "~> 0.15"}, {:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"}, {:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12.3"}, {:pbkdf2_elixir, "~> 0.12.3"},
@ -174,12 +183,13 @@ defp deps do
{:flake_id, "~> 0.1.0"}, {:flake_id, "~> 0.1.0"},
{:remote_ip, {:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git", git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"}, ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
{:captcha, {:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:mox, "~> 0.5", only: :test}, {:mox, "~> 0.5", only: :test},
{:restarter, path: "./restarter"} {:restarter, path: "./restarter"},
{:open_api_spex, "~> 3.6"}
] ++ oauth_deps() ] ++ oauth_deps()
end end

View file

@ -26,7 +26,7 @@
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"ecto": {:hex, :ecto, "3.3.3", "0830bf3aebcbf3d8c1a1811cd581773b6866886c012f52c0f027031fa96a0b53", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12e368e3c2a2938d7776defaabdae40e82900fc4d8d66120ec1e01dfd8b93c3a"}, "ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5eed18252f5b5bbadec56a24112b531343507dbe046273133176b12190ce19cc"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"}, "ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
"esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"}, "esshd": {:hex, :esshd, "0.1.1", "d4dd4c46698093a40a56afecce8a46e246eb35463c457c246dacba2e056f31b5", [:mix], [], "hexpm", "d73e341e3009d390aa36387dc8862860bf9f874c94d9fd92ade2926376f49981"},
@ -55,7 +55,7 @@
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"}, "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
@ -73,7 +73,8 @@
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "0.12.1", "695e9490c6e0edfca616d80639528e448bd29b3bff7b7dd10a56c79b00a5d7fb", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1d58d69b8b5a86e7167abbb8cc92764a66f25f12f6172052595067fc6a30a17"}, "oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
"open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
"phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"}, "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
@ -96,7 +97,7 @@
"quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"}, "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]}, "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"}, "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},

View file

@ -3,7 +3,6 @@ defmodule Pleroma.Repo.Migrations.MigrateOldBookmarks do
import Ecto.Query import Ecto.Query
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark alias Pleroma.Bookmark
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
def up do def up do

View file

@ -1,6 +1,5 @@
defmodule Pleroma.Repo.Migrations.CreateSafeJsonbSet do defmodule Pleroma.Repo.Migrations.CreateSafeJsonbSet do
use Ecto.Migration use Ecto.Migration
alias Pleroma.User
def change do def change do
execute(""" execute("""

View file

@ -0,0 +1,29 @@
defmodule Pleroma.Repo.Migrations.ChangeFollowingRelationshipsStateToInteger do
use Ecto.Migration
@alter_following_relationship_state "ALTER TABLE following_relationships ALTER COLUMN state"
def up do
execute("""
#{@alter_following_relationship_state} TYPE integer USING
CASE
WHEN state = 'pending' THEN 1
WHEN state = 'accept' THEN 2
WHEN state = 'reject' THEN 3
ELSE 0
END;
""")
end
def down do
execute("""
#{@alter_following_relationship_state} TYPE varchar(255) USING
CASE
WHEN state = 1 THEN 'pending'
WHEN state = 2 THEN 'accept'
WHEN state = 3 THEN 'reject'
ELSE ''
END;
""")
end
end

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.AddFollowingRelationshipsFollowingIdIndex do
use Ecto.Migration
# [:follower_index] index is useless because of [:follower_id, :following_id] index
# [:following_id] index makes sense because of user's followers-targeted queries
def change do
drop_if_exists(index(:following_relationships, [:follower_id]))
create_if_not_exists(index(:following_relationships, [:following_id]))
end
end

View file

@ -0,0 +1,11 @@
defmodule Pleroma.Repo.Migrations.UpdateObanJobsTable do
use Ecto.Migration
def up do
Oban.Migrations.up(version: 8)
end
def down do
Oban.Migrations.down(version: 7)
end
end

BIN
test/fixtures/emoji/packs/blank.png.zip vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,10 @@
{
"finmoji": {
"license": "CC BY-NC-ND 4.0",
"homepage": "https://finland.fi/emoji/",
"description": "Finland is the first country in the world to publish its own set of country themed emojis. The Finland emoji collection contains 56 tongue-in-cheek emotions, which were created to explain some hard-to-describe Finnish emotions, Finnish words and customs.",
"src": "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip",
"src_sha256": "384025A1AC6314473863A11AC7AB38A12C01B851A3F82359B89B4D4211D3291D",
"files": "finmoji.json"
}
}

View file

@ -0,0 +1,3 @@
{
"blank": "blank.png"
}

10
test/fixtures/emoji/packs/manifest.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"blobs.gg": {
"src_sha256": "3a12f3a181678d5b3584a62095411b0d60a335118135910d879920f8ade5a57f",
"src": "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip",
"license": "Apache 2.0",
"homepage": "https://blobs.gg",
"files": "blobs_gg.json",
"description": "Blob Emoji from blobs.gg repacked as apng"
}
}

View file

@ -15,28 +15,28 @@ defmodule Pleroma.FollowingRelationshipTest do
test "returns following addresses without internal.fetch" do test "returns following addresses without internal.fetch" do
user = insert(:user) user = insert(:user)
fetch_actor = InternalFetchActor.get_actor() fetch_actor = InternalFetchActor.get_actor()
FollowingRelationship.follow(fetch_actor, user, "accept") FollowingRelationship.follow(fetch_actor, user, :follow_accept)
assert FollowingRelationship.following(fetch_actor) == [user.follower_address] assert FollowingRelationship.following(fetch_actor) == [user.follower_address]
end end
test "returns following addresses without relay" do test "returns following addresses without relay" do
user = insert(:user) user = insert(:user)
relay_actor = Relay.get_actor() relay_actor = Relay.get_actor()
FollowingRelationship.follow(relay_actor, user, "accept") FollowingRelationship.follow(relay_actor, user, :follow_accept)
assert FollowingRelationship.following(relay_actor) == [user.follower_address] assert FollowingRelationship.following(relay_actor) == [user.follower_address]
end end
test "returns following addresses without remote user" do test "returns following addresses without remote user" do
user = insert(:user) user = insert(:user)
actor = insert(:user, local: false) actor = insert(:user, local: false)
FollowingRelationship.follow(actor, user, "accept") FollowingRelationship.follow(actor, user, :follow_accept)
assert FollowingRelationship.following(actor) == [user.follower_address] assert FollowingRelationship.following(actor) == [user.follower_address]
end end
test "returns following addresses with local user" do test "returns following addresses with local user" do
user = insert(:user) user = insert(:user)
actor = insert(:user, local: true) actor = insert(:user, local: true)
FollowingRelationship.follow(actor, user, "accept") FollowingRelationship.follow(actor, user, :follow_accept)
assert FollowingRelationship.following(actor) == [ assert FollowingRelationship.following(actor) == [
actor.follower_address, actor.follower_address,

View file

@ -150,13 +150,13 @@ test "gives a replacement for user links, using local nicknames in user links te
assert length(mentions) == 3 assert length(mentions) == 3
expected_text = expected_text =
~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{ ~s(<span class="h-card"><a class="u-url mention" data-user="#{gsimg.id}" href="#{
gsimg.ap_id gsimg.ap_id
}" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{ }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a class="u-url mention" data-user="#{
archaeme.id archaeme.id
}" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{ }" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a class="u-url mention" data-user="#{
archaeme_remote.id archaeme_remote.id
}" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>) }" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
assert expected_text == text assert expected_text == text
end end
@ -171,7 +171,7 @@ test "gives a replacement for user links when the user is using Osada" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{ ~s(<span class="h-card"><a class="u-url mention" data-user="#{mike.id}" href="#{
mike.ap_id mike.ap_id
}" rel="ugc">@<span>mike</span></a></span> test) }" rel="ugc">@<span>mike</span></a></span> test)
@ -187,7 +187,7 @@ test "gives a replacement for single-character local nicknames" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi) ~s(<span class="h-card"><a class="u-url mention" data-user="#{o.id}" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
assert expected_text == text assert expected_text == text
end end
@ -209,17 +209,13 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}] assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text == assert expected_text ==
~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{ ~s(<span class="h-card"><a class="u-url mention" data-user="#{user.id}" href="#{
user.ap_id user.ap_id
}" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{ }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{
other_user.id other_user.id
}" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{ }" href="#{other_user.ap_id}" rel="ugc">@<span>#{other_user.nickname}</span></a></span> hey dudes i hate <span class="h-card"><a class="u-url mention" data-user="#{
other_user.nickname
}</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
third_user.id third_user.id
}" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{ }" href="#{third_user.ap_id}" rel="ugc">@<span>#{third_user.nickname}</span></a></span>)
third_user.nickname
}</span></a></span>)
end end
test "given the 'safe_mention' option, it will still work without any mention" do test "given the 'safe_mention' option, it will still work without any mention" do

View file

@ -537,7 +537,7 @@ test "it does not send notification to mentioned users in likes" do
"status" => "hey @#{other_user.nickname}!" "status" => "hey @#{other_user.nickname}!"
}) })
{:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user) {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
{enabled_receivers, _disabled_receivers} = {enabled_receivers, _disabled_receivers} =
Notification.get_notified_from_activity(activity_two) Notification.get_notified_from_activity(activity_two)
@ -620,7 +620,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is de
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
@ -637,7 +637,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is un
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
{:ok, _, _} = CommonAPI.favorite(activity.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
@ -692,7 +692,7 @@ test "liking an activity which is already deleted does not generate a notificati
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.favorite(activity.id, other_user) {:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
end end

View file

@ -380,7 +380,8 @@ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
user = insert(:user) user = insert(:user)
activity = Activity.get_create_by_object_ap_id(object.data["id"]) activity = Activity.get_create_by_object_ap_id(object.data["id"])
{:ok, _activity, object} = CommonAPI.favorite(activity.id, user) {:ok, activity} = CommonAPI.favorite(user, activity.id)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 1 assert object.data["like_count"] == 1

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Plugs.RateLimiterTest do defmodule Pleroma.Plugs.RateLimiterTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Phoenix.ConnTest
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Plug.Conn
import Pleroma.Factory import Pleroma.Factory
import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2] import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2]
@ -36,8 +38,15 @@ test "config is required for plug to work" do
end end
test "it is disabled if it remote ip plug is enabled but no remote ip is found" do test "it is disabled if it remote ip plug is enabled but no remote ip is found" do
Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false))
assert RateLimiter.disabled?(Plug.Conn.assign(build_conn(), :remote_ip_found, false)) end
test "it is enabled if remote ip found" do
refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true))
end
test "it is enabled if remote_ip_found flag doesn't exist" do
refute RateLimiter.disabled?(build_conn())
end end
test "it restricts based on config values" do test "it restricts based on config values" do
@ -58,7 +67,7 @@ test "it restricts based on config values" do
end end
conn = RateLimiter.call(conn, plug_opts) conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
assert conn.halted assert conn.halted
Process.sleep(50) Process.sleep(50)
@ -68,7 +77,7 @@ test "it restricts based on config values" do
conn = RateLimiter.call(conn, plug_opts) conn = RateLimiter.call(conn, plug_opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
refute conn.status == Plug.Conn.Status.code(:too_many_requests) refute conn.status == Conn.Status.code(:too_many_requests)
refute conn.resp_body refute conn.resp_body
refute conn.halted refute conn.halted
end end
@ -98,7 +107,7 @@ test "`params` option allows different queries to be tracked independently" do
plug_opts = RateLimiter.init(name: limiter_name, params: ["id"]) plug_opts = RateLimiter.init(name: limiter_name, params: ["id"])
conn = build_conn(:get, "/?id=1") conn = build_conn(:get, "/?id=1")
conn = Plug.Conn.fetch_query_params(conn) conn = Conn.fetch_query_params(conn)
conn_2 = build_conn(:get, "/?id=2") conn_2 = build_conn(:get, "/?id=2")
RateLimiter.call(conn, plug_opts) RateLimiter.call(conn, plug_opts)
@ -119,7 +128,7 @@ test "it supports combination of options modifying bucket name" do
id = "100" id = "100"
conn = build_conn(:get, "/?id=#{id}") conn = build_conn(:get, "/?id=#{id}")
conn = Plug.Conn.fetch_query_params(conn) conn = Conn.fetch_query_params(conn)
conn_2 = build_conn(:get, "/?id=#{101}") conn_2 = build_conn(:get, "/?id=#{101}")
RateLimiter.call(conn, plug_opts) RateLimiter.call(conn, plug_opts)
@ -147,13 +156,13 @@ test "are restricted based on remote IP" do
conn = RateLimiter.call(conn, plug_opts) conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
assert conn.halted assert conn.halted
conn_2 = RateLimiter.call(conn_2, plug_opts) conn_2 = RateLimiter.call(conn_2, plug_opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests) refute conn_2.status == Conn.Status.code(:too_many_requests)
refute conn_2.resp_body refute conn_2.resp_body
refute conn_2.halted refute conn_2.halted
end end
@ -187,7 +196,7 @@ test "can have limits separate from unauthenticated connections" do
conn = RateLimiter.call(conn, plug_opts) conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
assert conn.halted assert conn.halted
end end
@ -210,12 +219,12 @@ test "different users are counted independently" do
end end
conn = RateLimiter.call(conn, plug_opts) conn = RateLimiter.call(conn, plug_opts)
assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests) assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
assert conn.halted assert conn.halted
conn_2 = RateLimiter.call(conn_2, plug_opts) conn_2 = RateLimiter.call(conn_2, plug_opts)
assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts) assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
refute conn_2.status == Plug.Conn.Status.code(:too_many_requests) refute conn_2.status == Conn.Status.code(:too_many_requests)
refute conn_2.resp_body refute conn_2.resp_body
refute conn_2.halted refute conn_2.halted
end end

View file

@ -60,7 +60,7 @@ test "doesn't count unrelated activities" do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) {:ok, activity} = CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"})
_ = CommonAPI.follow(user, other_user) _ = CommonAPI.follow(user, other_user)
CommonAPI.favorite(activity.id, other_user) CommonAPI.favorite(other_user, activity.id)
CommonAPI.repeat(activity.id, other_user) CommonAPI.repeat(activity.id, other_user)
assert %{direct: 0, private: 0, public: 1, unlisted: 0} = assert %{direct: 0, private: 0, public: 1, unlisted: 0} =

View file

@ -102,7 +102,7 @@ test "it turns OrderedCollection likes into empty arrays" do
{:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"}) {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"})
{:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"}) {:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"})
CommonAPI.favorite(id, user2) CommonAPI.favorite(user2, id)
likes = %{ likes = %{
"first" => "first" =>

226
test/tasks/emoji_test.exs Normal file
View file

@ -0,0 +1,226 @@
defmodule Mix.Tasks.Pleroma.EmojiTest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO
import Tesla.Mock
alias Mix.Tasks.Pleroma.Emoji
describe "ls-packs" do
test "with default manifest as url" do
mock(fn
%{
method: :get,
url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
}
end)
capture_io(fn -> Emoji.run(["ls-packs"]) end) =~
"https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
end
test "with passed manifest as file" do
capture_io(fn ->
Emoji.run(["ls-packs", "-m", "test/fixtures/emoji/packs/manifest.json"])
end) =~ "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip"
end
end
describe "get-packs" do
test "download pack from default manifest" do
mock(fn
%{
method: :get,
url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
}
%{
method: :get,
url: "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/blank.png.zip")
}
%{
method: :get,
url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/finmoji.json"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/finmoji.json")
}
end)
assert capture_io(fn -> Emoji.run(["get-packs", "finmoji"]) end) =~ "Writing pack.json for"
emoji_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
assert File.exists?(Path.join([emoji_path, "finmoji", "pack.json"]))
on_exit(fn -> File.rm_rf!("test/instance_static/emoji/finmoji") end)
end
test "pack not found" do
mock(fn
%{
method: :get,
url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/default-manifest.json")
}
end)
assert capture_io(fn -> Emoji.run(["get-packs", "not_found"]) end) =~
"No pack named \"not_found\" found"
end
test "raise on bad sha256" do
mock(fn
%{
method: :get,
url: "https://git.pleroma.social/pleroma/emoji-index/raw/master/packs/blobs_gg.zip"
} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/emoji/packs/blank.png.zip")
}
end)
assert_raise RuntimeError, ~r/^Bad SHA256 for blobs.gg/, fn ->
capture_io(fn ->
Emoji.run(["get-packs", "blobs.gg", "-m", "test/fixtures/emoji/packs/manifest.json"])
end)
end
end
end
describe "gen-pack" do
setup do
url = "https://finland.fi/wp-content/uploads/2017/06/finland-emojis.zip"
mock(fn %{
method: :get,
url: ^url
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/emoji/packs/blank.png.zip")}
end)
{:ok, url: url}
end
test "with default extensions", %{url: url} do
name = "pack1"
pack_json = "#{name}.json"
files_json = "#{name}_file.json"
refute File.exists?(pack_json)
refute File.exists?(files_json)
captured =
capture_io(fn ->
Emoji.run([
"gen-pack",
url,
"--name",
name,
"--license",
"license",
"--homepage",
"homepage",
"--description",
"description",
"--files",
files_json,
"--extensions",
".png .gif"
])
end)
assert captured =~ "#{pack_json} has been created with the pack1 pack"
assert captured =~ "Using .png .gif extensions"
assert File.exists?(pack_json)
assert File.exists?(files_json)
on_exit(fn ->
File.rm!(pack_json)
File.rm!(files_json)
end)
end
test "with custom extensions and update existing files", %{url: url} do
name = "pack2"
pack_json = "#{name}.json"
files_json = "#{name}_file.json"
refute File.exists?(pack_json)
refute File.exists?(files_json)
captured =
capture_io(fn ->
Emoji.run([
"gen-pack",
url,
"--name",
name,
"--license",
"license",
"--homepage",
"homepage",
"--description",
"description",
"--files",
files_json,
"--extensions",
" .png .gif .jpeg "
])
end)
assert captured =~ "#{pack_json} has been created with the pack2 pack"
assert captured =~ "Using .png .gif .jpeg extensions"
assert File.exists?(pack_json)
assert File.exists?(files_json)
captured =
capture_io(fn ->
Emoji.run([
"gen-pack",
url,
"--name",
name,
"--license",
"license",
"--homepage",
"homepage",
"--description",
"description",
"--files",
files_json,
"--extensions",
" .png .gif .jpeg "
])
end)
assert captured =~ "#{pack_json} has been updated with the pack2 pack"
on_exit(fn ->
File.rm!(pack_json)
File.rm!(files_json)
end)
end
end
end

View file

@ -140,7 +140,7 @@ test "no user to toggle" do
test "user is unsubscribed" do test "user is unsubscribed" do
followed = insert(:user) followed = insert(:user)
user = insert(:user) user = insert(:user)
User.follow(user, followed, "accept") User.follow(user, followed, :follow_accept)
Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname]) Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])

View file

@ -194,7 +194,8 @@ test "doesn't return already accepted or duplicate follow requests" do
CommonAPI.follow(pending_follower, locked) CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked) CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked) CommonAPI.follow(accepted_follower, locked)
Pleroma.FollowingRelationship.update(accepted_follower, locked, "accept")
Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept)
assert [^pending_follower] = User.get_follow_requests(locked) assert [^pending_follower] = User.get_follow_requests(locked)
end end
@ -319,7 +320,7 @@ test "unfollow with syncronizes external user" do
following_address: "http://localhost:4001/users/fuser2/following" following_address: "http://localhost:4001/users/fuser2/following"
}) })
{:ok, user} = User.follow(user, followed, "accept") {:ok, user} = User.follow(user, followed, :follow_accept)
{:ok, user, _activity} = User.unfollow(user, followed) {:ok, user, _activity} = User.unfollow(user, followed)
@ -332,7 +333,7 @@ test "unfollow takes a user and another user" do
followed = insert(:user) followed = insert(:user)
user = insert(:user) user = insert(:user)
{:ok, user} = User.follow(user, followed, "accept") {:ok, user} = User.follow(user, followed, :follow_accept)
assert User.following(user) == [user.follower_address, followed.follower_address] assert User.following(user) == [user.follower_address, followed.follower_address]
@ -353,7 +354,7 @@ test "unfollow doesn't unfollow yourself" do
test "test if a user is following another user" do test "test if a user is following another user" do
followed = insert(:user) followed = insert(:user)
user = insert(:user) user = insert(:user)
User.follow(user, followed, "accept") User.follow(user, followed, :follow_accept)
assert User.following?(user, followed) assert User.following?(user, followed)
refute User.following?(followed, user) refute User.following?(followed, user)
@ -1141,8 +1142,8 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
object_two = insert(:note, user: follower) object_two = insert(:note, user: follower)
activity_two = insert(:note_activity, user: follower, note: object_two) activity_two = insert(:note_activity, user: follower, note: object_two)
{:ok, like, _} = CommonAPI.favorite(activity_two.id, user) {:ok, like} = CommonAPI.favorite(user, activity_two.id)
{:ok, like_two, _} = CommonAPI.favorite(activity.id, follower) {:ok, like_two} = CommonAPI.favorite(follower, activity.id)
{:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user) {:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
{:ok, job} = User.delete(user) {:ok, job} = User.delete(user)
@ -1404,7 +1405,7 @@ test "preserves hosts in user links text" do
bio = "A.k.a. @nick@domain.com" bio = "A.k.a. @nick@domain.com"
expected_text = expected_text =
~s(A.k.a. <span class="h-card"><a data-user="#{remote_user.id}" class="u-url mention" href="#{ ~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{
remote_user.ap_id remote_user.ap_id
}" rel="ugc">@<span>nick@domain.com</span></a></span>) }" rel="ugc">@<span>nick@domain.com</span></a></span>)

View file

@ -1239,16 +1239,56 @@ test "POST /api/ap/upload_media", %{conn: conn} do
filename: "an_image.jpg" filename: "an_image.jpg"
} }
conn = object =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(:created)
assert object = json_response(conn, :created)
assert object["name"] == desc assert object["name"] == desc
assert object["type"] == "Document" assert object["type"] == "Document"
assert object["actor"] == user.ap_id assert object["actor"] == user.ap_id
assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
assert is_binary(object_href)
assert object_mediatype == "image/jpeg"
activity_request = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test, attachment",
"attachment" => [object]
},
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
activity_response =
conn
|> assign(:user, user)
|> post("/users/#{user.nickname}/outbox", activity_request)
|> json_response(:created)
assert activity_response["id"]
assert activity_response["object"]
assert activity_response["actor"] == user.ap_id
assert %Object{data: %{"attachment" => [attachment]}} =
Object.normalize(activity_response["object"])
assert attachment["type"] == "Document"
assert attachment["name"] == desc
assert [
%{
"href" => ^object_href,
"type" => "Link",
"mediaType" => ^object_mediatype
}
] = attachment["url"]
# Fails if unauthenticated
conn conn
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc}) |> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403) |> json_response(403)

View file

@ -1900,14 +1900,14 @@ test "returns a favourite activities sorted by adds to favorite" do
{:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "}) {:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
{:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "}) {:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
{:ok, _, _} = CommonAPI.favorite(a4.id, user) {:ok, _} = CommonAPI.favorite(user, a4.id)
{:ok, _, _} = CommonAPI.favorite(a3.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, a3.id)
{:ok, _, _} = CommonAPI.favorite(a3.id, user) {:ok, _} = CommonAPI.favorite(user, a3.id)
{:ok, _, _} = CommonAPI.favorite(a5.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, a5.id)
{:ok, _, _} = CommonAPI.favorite(a5.id, user) {:ok, _} = CommonAPI.favorite(user, a5.id)
{:ok, _, _} = CommonAPI.favorite(a4.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, a4.id)
{:ok, _, _} = CommonAPI.favorite(a1.id, user) {:ok, _} = CommonAPI.favorite(user, a1.id)
{:ok, _, _} = CommonAPI.favorite(a1.id, other_user) {:ok, _} = CommonAPI.favorite(other_user, a1.id)
result = ActivityPub.fetch_favourites(user) result = ActivityPub.fetch_favourites(user)
assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id] assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]

View file

@ -0,0 +1,83 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
describe "likes" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
valid_like = %{
"to" => [user.ap_id],
"cc" => [],
"type" => "Like",
"id" => Utils.generate_activity_id(),
"object" => post_activity.data["object"],
"actor" => user.ap_id,
"context" => "a context"
}
%{valid_like: valid_like, user: user, post_activity: post_activity}
end
test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
{:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
assert "id" in Map.keys(object)
end
test "is valid for a valid object", %{valid_like: valid_like} do
assert LikeValidator.cast_and_validate(valid_like).valid?
end
test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
without_actor = Map.delete(valid_like, "actor")
refute LikeValidator.cast_and_validate(without_actor).valid?
with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
end
test "it errors when the object is missing or not known", %{valid_like: valid_like} do
without_object = Map.delete(valid_like, "object")
refute LikeValidator.cast_and_validate(without_object).valid?
with_invalid_object = Map.put(valid_like, "object", "invalidobject")
refute LikeValidator.cast_and_validate(with_invalid_object).valid?
end
test "it errors when the actor has already like the object", %{
valid_like: valid_like,
user: user,
post_activity: post_activity
} do
_like = CommonAPI.favorite(user, post_activity.id)
refute LikeValidator.cast_and_validate(valid_like).valid?
end
test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
wrapped_like =
valid_like
|> Map.put("actor", %{"id" => valid_like["actor"]})
|> Map.put("object", %{"id" => valid_like["object"]})
validated = LikeValidator.cast_and_validate(wrapped_like)
assert validated.valid?
assert {:actor, valid_like["actor"]} in validated.changes
assert {:object, valid_like["object"]} in validated.changes
end
end
end

View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidatorTest do
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
alias Pleroma.Web.ActivityPub.Utils
import Pleroma.Factory
describe "Notes" do
setup do
user = insert(:user)
note = %{
"id" => Utils.generate_activity_id(),
"type" => "Note",
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => [],
"content" => "Hellow this is content.",
"context" => "xxx",
"summary" => "a post"
}
%{user: user, note: note}
end
test "a basic note validates", %{note: note} do
%{valid?: true} = NoteValidator.cast_and_validate(note)
end
end
end

View file

@ -0,0 +1,32 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTimeTest do
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime
use Pleroma.DataCase
test "it validates an xsd:Datetime" do
valid_strings = [
"2004-04-12T13:20:00",
"2004-04-12T13:20:15.5",
"2004-04-12T13:20:00-05:00",
"2004-04-12T13:20:00Z"
]
invalid_strings = [
"2004-04-12T13:00",
"2004-04-1213:20:00",
"99-04-12T13:00",
"2004-04-12"
]
assert {:ok, "2004-04-01T12:00:00Z"} == DateTime.cast("2004-04-01T12:00:00Z")
Enum.each(valid_strings, fn date_time ->
result = DateTime.cast(date_time)
assert {:ok, _} = result
end)
Enum.each(invalid_strings, fn date_time ->
result = DateTime.cast(date_time)
assert :error == result
end)
end
end

View file

@ -0,0 +1,37 @@
defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
use Pleroma.DataCase
@uris [
"http://lain.com/users/lain",
"http://lain.com",
"https://lain.com/object/1"
]
@non_uris [
"https://",
"rin",
1,
:x,
%{"1" => 2}
]
test "it accepts http uris" do
Enum.each(@uris, fn uri ->
assert {:ok, uri} == ObjectID.cast(uri)
end)
end
test "it accepts an object with a nested uri id" do
Enum.each(@uris, fn uri ->
assert {:ok, uri} == ObjectID.cast(%{"id" => uri})
end)
end
test "it rejects non-uri strings" do
Enum.each(@non_uris, fn non_uri ->
assert :error == ObjectID.cast(non_uri)
assert :error == ObjectID.cast(%{"id" => non_uri})
end)
end
end

View file

@ -0,0 +1,87 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.PipelineTest do
use Pleroma.DataCase
import Mock
import Pleroma.Factory
describe "common_pipeline/2" do
test "it goes through validation, filtering, persisting, side effects and federation for local activities" do
activity = insert(:note_activity)
meta = [local: true]
with_mocks([
{Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]},
{
Pleroma.Web.ActivityPub.MRF,
[],
[filter: fn o -> {:ok, o} end]
},
{
Pleroma.Web.ActivityPub.ActivityPub,
[],
[persist: fn o, m -> {:ok, o, m} end]
},
{
Pleroma.Web.ActivityPub.SideEffects,
[],
[handle: fn o, m -> {:ok, o, m} end]
},
{
Pleroma.Web.Federator,
[],
[publish: fn _o -> :ok end]
}
]) do
assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
assert_called(Pleroma.Web.Federator.publish(activity))
end
end
test "it goes through validation, filtering, persisting, side effects without federation for remote activities" do
activity = insert(:note_activity)
meta = [local: false]
with_mocks([
{Pleroma.Web.ActivityPub.ObjectValidator, [], [validate: fn o, m -> {:ok, o, m} end]},
{
Pleroma.Web.ActivityPub.MRF,
[],
[filter: fn o -> {:ok, o} end]
},
{
Pleroma.Web.ActivityPub.ActivityPub,
[],
[persist: fn o, m -> {:ok, o, m} end]
},
{
Pleroma.Web.ActivityPub.SideEffects,
[],
[handle: fn o, m -> {:ok, o, m} end]
},
{
Pleroma.Web.Federator,
[],
[]
}
]) do
assert {:ok, ^activity, ^meta} =
Pleroma.Web.ActivityPub.Pipeline.common_pipeline(activity, meta)
assert_called(Pleroma.Web.ActivityPub.ObjectValidator.validate(activity, meta))
assert_called(Pleroma.Web.ActivityPub.MRF.filter(activity))
assert_called(Pleroma.Web.ActivityPub.ActivityPub.persist(activity, meta))
assert_called(Pleroma.Web.ActivityPub.SideEffects.handle(activity, meta))
end
end
end
end

View file

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
describe "like objects" do
setup do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, like_data, _meta} = Builder.like(user, post.object)
{:ok, like, _meta} = ActivityPub.persist(like_data, local: true)
%{like: like, user: user}
end
test "add the like to the original object", %{like: like, user: user} do
{:ok, like, _} = SideEffects.handle(like)
object = Object.get_by_ap_id(like.data["object"])
assert object.data["like_count"] == 1
assert user.ap_id in object.data["likes"]
end
end
end

View file

@ -334,7 +334,9 @@ test "it works for incoming likes" do
|> Poison.decode!() |> Poison.decode!()
|> Map.put("object", activity.data["object"]) |> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data)
refute Enum.empty?(activity.recipients)
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like" assert data["type"] == "Like"
@ -1228,19 +1230,13 @@ test "it remaps video URLs as attachments if necessary" do
attachment = %{ attachment = %{
"type" => "Link", "type" => "Link",
"mediaType" => "video/mp4", "mediaType" => "video/mp4",
"href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mimeType" => "video/mp4",
"size" => 5_015_880,
"url" => [ "url" => [
%{ %{
"href" => "href" =>
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mediaType" => "video/mp4", "mediaType" => "video/mp4"
"type" => "Link"
} }
], ]
"width" => 480
} }
assert object.data["url"] == assert object.data["url"] ==
@ -1622,7 +1618,7 @@ test "it upgrades a user to activitypub" do
}) })
user_two = insert(:user) user_two = insert(:user)
Pleroma.FollowingRelationship.follow(user_two, user, "accept") Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept)
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"}) {:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
@ -2061,11 +2057,7 @@ test "returns modified object when attachment is map" do
%{ %{
"mediaType" => "video/mp4", "mediaType" => "video/mp4",
"url" => [ "url" => [
%{ %{"href" => "https://peertube.moe/stat-480.mp4", "mediaType" => "video/mp4"}
"href" => "https://peertube.moe/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
] ]
} }
] ]
@ -2083,23 +2075,13 @@ test "returns modified object when attachment is list" do
%{ %{
"mediaType" => "video/mp4", "mediaType" => "video/mp4",
"url" => [ "url" => [
%{ %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
] ]
}, },
%{ %{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4", "mediaType" => "video/mp4",
"mimeType" => "video/mp4",
"url" => [ "url" => [
%{ %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
] ]
} }
] ]

View file

@ -59,7 +59,7 @@ test "renders a like activity" do
object = Object.normalize(note) object = Object.normalize(note)
user = insert(:user) user = insert(:user)
{:ok, like_activity, _} = CommonAPI.favorite(note.id, user) {:ok, like_activity} = CommonAPI.favorite(user, note.id)
result = ObjectView.render("object.json", %{object: like_activity}) result = ObjectView.render("object.json", %{object: like_activity})

View file

@ -625,6 +625,39 @@ test "it returns 403 if requested by a non-admin" do
assert json_response(conn, :forbidden) assert json_response(conn, :forbidden)
end end
test "email with +", %{conn: conn, admin: admin} do
recipient_email = "foo+bar@baz.com"
conn
|> put_req_header("content-type", "application/json;charset=utf-8")
|> post("/api/pleroma/admin/users/email_invite", %{email: recipient_email})
|> json_response(:no_content)
token_record =
Pleroma.UserInviteToken
|> Repo.all()
|> List.last()
assert token_record
refute token_record.used
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
email =
Pleroma.Emails.UserEmail.user_invitation_email(
admin,
token_record,
recipient_email
)
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: recipient_email,
html_body: email.html_body
)
end
end end
describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do
@ -637,7 +670,8 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error) assert json_response(conn, :bad_request) ==
"To send invites you need to set the `invites_enabled` option to true."
end end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
@ -646,7 +680,8 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error) assert json_response(conn, :bad_request) ==
"To send invites you need to set the `registrations_open` option to false."
end end
end end
@ -2238,13 +2273,17 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do
value: :erlang.term_to_binary([]) value: :erlang.term_to_binary([])
) )
Pleroma.Config.TransferTask.load_and_update_env([], false)
assert Application.get_env(:logger, :backends) == []
conn = conn =
post(conn, "/api/pleroma/admin/config", %{ post(conn, "/api/pleroma/admin/config", %{
configs: [ configs: [
%{ %{
group: config.group, group: config.group,
key: config.key, key: config.key,
value: [":console", %{"tuple" => ["ExSyslogger", ":ex_syslogger"]}] value: [":console"]
} }
] ]
}) })
@ -2255,8 +2294,7 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do
"group" => ":logger", "group" => ":logger",
"key" => ":backends", "key" => ":backends",
"value" => [ "value" => [
":console", ":console"
%{"tuple" => ["ExSyslogger", ":ex_syslogger"]}
], ],
"db" => [":backends"] "db" => [":backends"]
} }
@ -2264,14 +2302,8 @@ test "saving full setting if value is in full_key_update list", %{conn: conn} do
} }
assert Application.get_env(:logger, :backends) == [ assert Application.get_env(:logger, :backends) == [
:console, :console
{ExSyslogger, :ex_syslogger}
] ]
capture_log(fn ->
require Logger
Logger.warn("Ooops...")
end) =~ "Ooops..."
end end
test "saving full setting if value is not keyword", %{conn: conn} do test "saving full setting if value is not keyword", %{conn: conn} do

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AppOperationTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
import OpenApiSpex.TestAssertions
import Pleroma.Factory
test "AppCreateRequest example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateRequest.schema()
assert_schema(schema.example, "AppCreateRequest", api_spec)
end
test "AppCreateResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateResponse.schema()
assert_schema(schema.example, "AppCreateResponse", api_spec)
end
test "AppController produces a AppCreateResponse", %{conn: conn} do
api_spec = ApiSpec.spec()
app_attrs = build(:oauth_app)
json =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/apps",
Jason.encode!(%{
client_name: app_attrs.client_name,
redirect_uris: app_attrs.redirect_uris
})
)
|> json_response(200)
assert_schema(json, "AppCreateResponse", api_spec)
end
end

View file

@ -284,9 +284,12 @@ test "favoriting a status" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, post_activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user) {:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
assert data["type"] == "Like"
assert data["actor"] == user.ap_id
assert data["object"] == post_activity.data["object"]
end end
test "retweeting a status twice returns the status" do test "retweeting a status twice returns the status" do
@ -298,13 +301,13 @@ test "retweeting a status twice returns the status" do
{:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user) {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user)
end end
test "favoriting a status twice returns the status" do test "favoriting a status twice returns ok, but without the like activity" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = activity, object} = CommonAPI.favorite(activity.id, user) {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
{:ok, ^activity, ^object} = CommonAPI.favorite(activity.id, user) assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
end end
end end
@ -562,7 +565,7 @@ test "cancels a pending follow for a local user" do
assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed) CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == "pending" assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil assert User.get_follow_state(follower, followed) == nil
@ -584,7 +587,7 @@ test "cancels a pending follow for a remote user" do
assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed) CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == "pending" assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil assert User.get_follow_state(follower, followed) == nil

Some files were not shown because too many files have changed in this diff Show more