From 37a1001b97be5b103dab9dc0f62d73487e8d5450 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Sun, 14 Aug 2022 23:13:49 +0000
Subject: [PATCH 01/44] add finch outbound proxy support (#158)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/158
---
CHANGELOG.md | 5 ++
config/description.exs | 7 +-
docs/docs/configuration/cheatsheet.md | 2 +-
lib/pleroma/application.ex | 4 +
lib/pleroma/http/adapter_helper.ex | 76 ++++++++++++-----
lib/pleroma/http/adapter_helper/default.ex | 2 +-
lib/pleroma/http/adapter_helper/gun.ex | 82 -------------------
lib/pleroma/http/adapter_helper/hackney.ex | 40 ---------
test/pleroma/http/adapter_helper/gun_test.exs | 77 -----------------
.../http/adapter_helper/hackney_test.exs | 35 --------
test/pleroma/http/adapter_helper_test.exs | 30 ++++++-
11 files changed, 98 insertions(+), 262 deletions(-)
delete mode 100644 lib/pleroma/http/adapter_helper/gun.ex
delete mode 100644 lib/pleroma/http/adapter_helper/hackney.ex
delete mode 100644 test/pleroma/http/adapter_helper/gun_test.exs
delete mode 100644 test/pleroma/http/adapter_helper/hackney_test.exs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c658af460..7c7cd8601 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Removed
+- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
+
+## 2022.08
+
### Added
- extended runtime module support, see config cheatsheet
- quote posting; quotes are limited to public posts
diff --git a/config/description.exs b/config/description.exs
index a44ab2432..9f93265d1 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2598,9 +2598,10 @@
%{
key: :proxy_url,
label: "Proxy URL",
- type: [:string, :tuple],
- description: "Proxy URL",
- suggestions: ["localhost:9020", {:socks5, :localhost, 3090}]
+ type: :string,
+ description:
+ "Proxy URL - of the format http://host:port. Advise setting in .exs instead of admin-fe due to this being set at boot-time.",
+ suggestions: ["http://localhost:3128"]
},
%{
key: :user_agent,
diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md
index 71ebf28dc..8fa188de1 100644
--- a/docs/docs/configuration/cheatsheet.md
+++ b/docs/docs/configuration/cheatsheet.md
@@ -521,7 +521,7 @@ Available caches:
### :http
-* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
+* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`); for example `http://127.0.0.1:3192`. Does not support SOCKS5 proxy, only http(s).
* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `adapter`: array of adapter options
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index e29bf3ca3..cb619232f 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -248,9 +248,13 @@ def limiters_setup do
end
defp http_children do
+ proxy_url = Config.get([:http, :proxy_url])
+ proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
+
config =
[:http, :adapter]
|> Config.get([])
+ |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|> Keyword.put(:name, MyFinch)
[{Finch, config}]
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
index f9b489616..4949dd727 100644
--- a/lib/pleroma/http/adapter_helper.ex
+++ b/lib/pleroma/http/adapter_helper.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.HTTP.AdapterHelper do
@moduledoc """
Configure Tesla.Client with default and customized adapter options.
"""
- @defaults [name: MyFinch, connect_timeout: 5_000, recv_timeout: 5_000]
+ @defaults [name: MyFinch, pool_timeout: 5_000, receive_timeout: 5_000]
@type proxy_type() :: :socks4 | :socks5
@type host() :: charlist() | :inet.ip_address()
@@ -25,15 +25,58 @@ def format_proxy(nil), do: nil
def format_proxy(proxy_url) do
case parse_proxy(proxy_url) do
- {:ok, host, port} -> {host, port}
- {:ok, type, host, port} -> {type, host, port}
+ {:ok, host, port} -> {:http, host, port, []}
+ {:ok, type, host, port} -> {type, host, port, []}
_ -> nil
end
end
@spec maybe_add_proxy(keyword(), proxy() | nil) :: keyword()
def maybe_add_proxy(opts, nil), do: opts
- def maybe_add_proxy(opts, proxy), do: Keyword.put_new(opts, :proxy, proxy)
+
+ def maybe_add_proxy(opts, proxy) do
+ Keyword.put(opts, :proxy, proxy)
+ end
+
+ def maybe_add_proxy_pool(opts, nil), do: opts
+
+ def maybe_add_proxy_pool(opts, proxy) do
+ Logger.info("Using HTTP Proxy: #{inspect(proxy)}")
+
+ opts
+ |> maybe_add_pools()
+ |> maybe_add_default_pool()
+ |> maybe_add_conn_opts()
+ |> put_in([:pools, :default, :conn_opts, :proxy], proxy)
+ end
+
+ defp maybe_add_pools(opts) do
+ if Keyword.has_key?(opts, :pools) do
+ opts
+ else
+ Keyword.put(opts, :pools, %{})
+ end
+ end
+
+ defp maybe_add_default_pool(opts) do
+ pools = Keyword.get(opts, :pools)
+
+ if Map.has_key?(pools, :default) do
+ opts
+ else
+ put_in(opts, [:pools, :default], [])
+ end
+ end
+
+ defp maybe_add_conn_opts(opts) do
+ conn_opts = get_in(opts, [:pools, :default, :conn_opts])
+
+ unless is_nil(conn_opts) do
+ opts
+ else
+ put_in(opts, [:pools, :default, :conn_opts], [])
+ end
+ end
@doc """
Merge default connection & adapter options with received ones.
@@ -46,36 +89,31 @@ def options(%URI{} = uri, opts \\ []) do
|> AdapterHelper.Default.options(uri)
end
+ defp proxy_type("http"), do: {:ok, :http}
+ defp proxy_type("https"), do: {:ok, :https}
+ defp proxy_type(_), do: {:error, :unknown}
+
@spec parse_proxy(String.t() | tuple() | nil) ::
{:ok, host(), pos_integer()}
| {:ok, proxy_type(), host(), pos_integer()}
| {:error, atom()}
| nil
-
def parse_proxy(nil), do: nil
def parse_proxy(proxy) when is_binary(proxy) do
- with [host, port] <- String.split(proxy, ":"),
- {port, ""} <- Integer.parse(port) do
- {:ok, parse_host(host), port}
+ with %URI{} = uri <- URI.parse(proxy),
+ {:ok, type} <- proxy_type(uri.scheme) do
+ {:ok, type, uri.host, uri.port}
else
- {_, _} ->
- Logger.warn("Parsing port failed #{inspect(proxy)}")
- {:error, :invalid_proxy_port}
-
- :error ->
- Logger.warn("Parsing port failed #{inspect(proxy)}")
- {:error, :invalid_proxy_port}
-
- _ ->
- Logger.warn("Parsing proxy failed #{inspect(proxy)}")
+ e ->
+ Logger.warn("Parsing proxy failed #{inspect(proxy)}, #{inspect(e)}")
{:error, :invalid_proxy}
end
end
def parse_proxy(proxy) when is_tuple(proxy) do
with {type, host, port} <- proxy do
- {:ok, type, parse_host(host), port}
+ {:ok, type, host, port}
else
_ ->
Logger.warn("Parsing proxy failed #{inspect(proxy)}")
diff --git a/lib/pleroma/http/adapter_helper/default.ex b/lib/pleroma/http/adapter_helper/default.ex
index a1614b9c5..630536871 100644
--- a/lib/pleroma/http/adapter_helper/default.ex
+++ b/lib/pleroma/http/adapter_helper/default.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Default do
@spec options(keyword(), URI.t()) :: keyword()
def options(opts, _uri) do
- proxy = Pleroma.Config.get([:http, :proxy_url], nil)
+ proxy = Pleroma.Config.get([:http, :proxy_url])
AdapterHelper.maybe_add_proxy(opts, AdapterHelper.format_proxy(proxy))
end
diff --git a/lib/pleroma/http/adapter_helper/gun.ex b/lib/pleroma/http/adapter_helper/gun.ex
deleted file mode 100644
index 251539f34..000000000
--- a/lib/pleroma/http/adapter_helper/gun.ex
+++ /dev/null
@@ -1,82 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.Gun do
- @behaviour Pleroma.HTTP.AdapterHelper
-
- alias Pleroma.Config
- alias Pleroma.HTTP.AdapterHelper
-
- require Logger
-
- @defaults [
- retry: 1,
- retry_timeout: 1_000
- ]
-
- @type pool() :: :federation | :upload | :media | :default
-
- @spec options(keyword(), URI.t()) :: keyword()
- def options(incoming_opts \\ [], %URI{} = uri) do
- proxy =
- [:http, :proxy_url]
- |> Config.get()
- |> AdapterHelper.format_proxy()
-
- config_opts = Config.get([:http, :adapter], [])
-
- @defaults
- |> Keyword.merge(config_opts)
- |> add_scheme_opts(uri)
- |> AdapterHelper.maybe_add_proxy(proxy)
- |> Keyword.merge(incoming_opts)
- |> put_timeout()
- end
-
- defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
-
- defp add_scheme_opts(opts, %{scheme: "https"}) do
- Keyword.put(opts, :certificates_verification, true)
- end
-
- defp put_timeout(opts) do
- {recv_timeout, opts} = Keyword.pop(opts, :recv_timeout, pool_timeout(opts[:pool]))
- # this is the timeout to receive a message from Gun
- # `:timeout` key is used in Tesla
- Keyword.put(opts, :timeout, recv_timeout)
- end
-
- @spec pool_timeout(pool()) :: non_neg_integer()
- def pool_timeout(pool) do
- default = Config.get([:pools, :default, :recv_timeout], 5_000)
-
- Config.get([:pools, pool, :recv_timeout], default)
- end
-
- def limiter_setup do
- prefix = Pleroma.Gun.ConnectionPool
- wait = Config.get([:connections_pool, :connection_acquisition_wait])
- retries = Config.get([:connections_pool, :connection_acquisition_retries])
-
- :pools
- |> Config.get([])
- |> Enum.each(fn {name, opts} ->
- max_running = Keyword.get(opts, :size, 50)
- max_waiting = Keyword.get(opts, :max_waiting, 10)
-
- result =
- ConcurrentLimiter.new(:"#{prefix}.#{name}", max_running, max_waiting,
- wait: wait,
- max_retries: retries
- )
-
- case result do
- :ok -> :ok
- {:error, :existing} -> :ok
- end
- end)
-
- :ok
- end
-end
diff --git a/lib/pleroma/http/adapter_helper/hackney.ex b/lib/pleroma/http/adapter_helper/hackney.ex
deleted file mode 100644
index af0ada1e7..000000000
--- a/lib/pleroma/http/adapter_helper/hackney.ex
+++ /dev/null
@@ -1,40 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.Hackney do
- @behaviour Pleroma.HTTP.AdapterHelper
-
- @defaults [
- follow_redirect: true,
- force_redirect: true
- ]
-
- @spec options(keyword(), URI.t()) :: keyword()
- def options(connection_opts \\ [], %URI{} = uri) do
- proxy = Pleroma.Config.get([:http, :proxy_url])
-
- config_opts = Pleroma.Config.get([:http, :adapter], [])
-
- @defaults
- |> Keyword.merge(config_opts)
- |> Keyword.merge(connection_opts)
- |> add_scheme_opts(uri)
- |> maybe_add_with_body()
- |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy(proxy)
- end
-
- defp add_scheme_opts(opts, %URI{scheme: "https"}) do
- Keyword.put(opts, :ssl_options, versions: [:"tlsv1.3", :"tlsv1.2", :"tlsv1.1", :tlsv1])
- end
-
- defp add_scheme_opts(opts, _), do: opts
-
- defp maybe_add_with_body(opts) do
- if opts[:max_body] do
- Keyword.put(opts, :with_body, true)
- else
- opts
- end
- end
-end
diff --git a/test/pleroma/http/adapter_helper/gun_test.exs b/test/pleroma/http/adapter_helper/gun_test.exs
deleted file mode 100644
index cfb68557d..000000000
--- a/test/pleroma/http/adapter_helper/gun_test.exs
+++ /dev/null
@@ -1,77 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.GunTest do
- use ExUnit.Case
- use Pleroma.Tests.Helpers
-
- import Mox
-
- alias Pleroma.HTTP.AdapterHelper.Gun
-
- setup :verify_on_exit!
-
- describe "options/1" do
- setup do: clear_config([:http, :adapter], a: 1, b: 2)
-
- test "https url with default port" do
- uri = URI.parse("https://example.com")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- end
-
- test "https ipv4 with default port" do
- uri = URI.parse("https://127.0.0.1")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- end
-
- test "https ipv6 with default port" do
- uri = URI.parse("https://[2a03:2880:f10c:83:face:b00c:0:25de]")
-
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:certificates_verification]
- end
-
- test "https url with non standart port" do
- uri = URI.parse("https://example.com:115")
-
- opts = Gun.options([receive_conn: false], uri)
-
- assert opts[:certificates_verification]
- end
-
- test "merges with defaul http adapter config" do
- defaults = Gun.options([receive_conn: false], URI.parse("https://example.com"))
- assert Keyword.has_key?(defaults, :a)
- assert Keyword.has_key?(defaults, :b)
- end
-
- test "parses string proxy host & port" do
- clear_config([:http, :proxy_url], "localhost:8123")
-
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:proxy] == {'localhost', 8123}
- end
-
- test "parses tuple proxy scheme host and port" do
- clear_config([:http, :proxy_url], {:socks, 'localhost', 1234})
-
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false], uri)
- assert opts[:proxy] == {:socks, 'localhost', 1234}
- end
-
- test "passed opts have more weight than defaults" do
- clear_config([:http, :proxy_url], {:socks5, 'localhost', 1234})
- uri = URI.parse("https://some-domain.com")
- opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri)
-
- assert opts[:proxy] == {'example.com', 4321}
- end
- end
-end
diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs
deleted file mode 100644
index 85150a65c..000000000
--- a/test/pleroma/http/adapter_helper/hackney_test.exs
+++ /dev/null
@@ -1,35 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
- use ExUnit.Case, async: true
- use Pleroma.Tests.Helpers
-
- alias Pleroma.HTTP.AdapterHelper.Hackney
-
- setup_all do
- uri = URI.parse("http://domain.com")
- {:ok, uri: uri}
- end
-
- describe "options/2" do
- setup do: clear_config([:http, :adapter], a: 1, b: 2)
-
- test "add proxy and opts from config", %{uri: uri} do
- opts = Hackney.options([proxy: "localhost:8123"], uri)
-
- assert opts[:a] == 1
- assert opts[:b] == 2
- assert opts[:proxy] == "localhost:8123"
- end
-
- test "respect connection opts and no proxy", %{uri: uri} do
- opts = Hackney.options([a: 2, b: 1], uri)
-
- assert opts[:a] == 2
- assert opts[:b] == 1
- refute Keyword.has_key?(opts, :proxy)
- end
- end
-end
diff --git a/test/pleroma/http/adapter_helper_test.exs b/test/pleroma/http/adapter_helper_test.exs
index 3c8c89164..55ffe4921 100644
--- a/test/pleroma/http/adapter_helper_test.exs
+++ b/test/pleroma/http/adapter_helper_test.exs
@@ -13,16 +13,38 @@ test "with nil" do
end
test "with string" do
- assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123}
+ assert AdapterHelper.format_proxy("http://127.0.0.1:8123") == {:http, "127.0.0.1", 8123, []}
end
test "localhost with port" do
- assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123}
+ assert AdapterHelper.format_proxy("https://localhost:8123") ==
+ {:https, "localhost", 8123, []}
end
test "tuple" do
- assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) ==
- {:socks4, 'localhost', 9050}
+ assert AdapterHelper.format_proxy({:http, "localhost", 9050}) ==
+ {:http, "localhost", 9050, []}
+ end
+ end
+
+ describe "maybe_add_proxy_pool/1" do
+ test "should do nothing with nil" do
+ assert AdapterHelper.maybe_add_proxy_pool([], nil) == []
+ end
+
+ test "should create pools" do
+ assert AdapterHelper.maybe_add_proxy_pool([], "proxy") == [
+ pools: %{default: [conn_opts: [proxy: "proxy"]]}
+ ]
+ end
+
+ test "should not override conn_opts if set" do
+ assert AdapterHelper.maybe_add_proxy_pool(
+ [pools: %{default: [conn_opts: [already: "set"]]}],
+ "proxy"
+ ) == [
+ pools: %{default: [conn_opts: [proxy: "proxy", already: "set"]]}
+ ]
end
end
end
From 61641957cb4cc42f52b4efe41d1a251fe235d42f Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Tue, 16 Aug 2022 22:56:49 +0000
Subject: [PATCH 02/44] fix compatibility with meilisearch (#164)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/164
---
CHANGELOG.md | 4 ++++
lib/mix/tasks/pleroma/search/meilisearch.ex | 8 ++++----
lib/pleroma/release_tasks.ex | 9 ++++++++-
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c7cd8601..3618211e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Fixed
+- Compatibility with latest meilisearch
+- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
+
### Removed
- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
diff --git a/lib/mix/tasks/pleroma/search/meilisearch.ex b/lib/mix/tasks/pleroma/search/meilisearch.ex
index d4a83c3cd..27a31afcf 100644
--- a/lib/mix/tasks/pleroma/search/meilisearch.ex
+++ b/lib/mix/tasks/pleroma/search/meilisearch.ex
@@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
import Ecto.Query
import Pleroma.Search.Meilisearch,
- only: [meili_post: 2, meili_put: 2, meili_get: 1, meili_delete!: 1]
+ only: [meili_put: 2, meili_get: 1, meili_delete!: 1]
def run(["index"]) do
start_pleroma()
@@ -27,7 +27,7 @@ def run(["index"]) do
end
{:ok, _} =
- meili_post(
+ meili_put(
"/indexes/objects/settings/ranking-rules",
[
"published:desc",
@@ -41,7 +41,7 @@ def run(["index"]) do
)
{:ok, _} =
- meili_post(
+ meili_put(
"/indexes/objects/settings/searchable-attributes",
[
"content"
@@ -91,7 +91,7 @@ def run(["index"]) do
)
with {:ok, res} <- result do
- if not Map.has_key?(res, "uid") do
+ if not Map.has_key?(res, "indexUid") do
IO.puts("\nFailed to index: #{inspect(result)}")
end
else
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
index 1e06aafe4..e43eef070 100644
--- a/lib/pleroma/release_tasks.ex
+++ b/lib/pleroma/release_tasks.ex
@@ -25,7 +25,7 @@ defp mix_task(task, args) do
module = Module.split(module)
match?(["Mix", "Tasks", "Pleroma" | _], module) and
- String.downcase(List.last(module)) == task
+ task_match?(module, task)
end)
if module do
@@ -35,6 +35,13 @@ defp mix_task(task, args) do
end
end
+ defp task_match?(["Mix", "Tasks", "Pleroma" | module_path], task) do
+ module_path
+ |> Enum.join(".")
+ |> String.downcase()
+ |> String.equivalent?(String.downcase(task))
+ end
+
def migrate(args) do
Mix.Tasks.Pleroma.Ecto.Migrate.run(args)
end
From 89ffc01c23cf89dc88245eb95f035e0404a5fcbb Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Tue, 16 Aug 2022 23:24:19 +0000
Subject: [PATCH 03/44] only return create objects for ES search (#165)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/165
---
CHANGELOG.md | 1 +
lib/pleroma/search/elasticsearch.ex | 10 +++++++---
lib/pleroma/search/elasticsearch/store.ex | 1 -
3 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3618211e4..c1e1d01c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Compatibility with latest meilisearch
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
+- Elasticsearch returning likes and repeats, displaying as posts
### Removed
- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
diff --git a/lib/pleroma/search/elasticsearch.ex b/lib/pleroma/search/elasticsearch.ex
index 7c7ca82c8..16b01101a 100644
--- a/lib/pleroma/search/elasticsearch.ex
+++ b/lib/pleroma/search/elasticsearch.ex
@@ -23,7 +23,7 @@ def es_query(:activity, query, offset, limit) do
timeout: "5s",
sort: [
"_score",
- %{_timestamp: %{order: "desc", format: "basic_date_time"}}
+ %{"_timestamp" => %{order: "desc", format: "basic_date_time"}}
],
query: %{
bool: %{
@@ -62,8 +62,12 @@ def search(user, query, options) do
Task.async(fn ->
q = es_query(:activity, parsed_query, offset, limit)
- Pleroma.Search.Elasticsearch.Store.search(:activities, q)
- |> Enum.filter(fn x -> Visibility.visible_for_user?(x, user) end)
+ :activities
+ |> Pleroma.Search.Elasticsearch.Store.search(q)
+ |> Enum.filter(fn x ->
+ x.data["type"] == "Create" && x.object.data["type"] == "Note" &&
+ Visibility.visible_for_user?(x, user)
+ end)
end)
activity_results = Task.await(activity_task)
diff --git a/lib/pleroma/search/elasticsearch/store.ex b/lib/pleroma/search/elasticsearch/store.ex
index 895b76d7f..3b7bbb838 100644
--- a/lib/pleroma/search/elasticsearch/store.ex
+++ b/lib/pleroma/search/elasticsearch/store.ex
@@ -42,7 +42,6 @@ def search(:activities, q) do
results
|> Enum.map(fn result -> result["_id"] end)
|> Pleroma.Activity.all_by_ids_with_object()
- |> Enum.sort(&(&1.inserted_at >= &2.inserted_at))
else
e ->
Logger.error(e)
From 11ec9daa5b742f8a1b408497321392e144f45019 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Wed, 17 Aug 2022 00:22:59 +0000
Subject: [PATCH 04/44] API compatibility with fedibird, frontend config (#163)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/163
---
CHANGELOG.md | 3 +
config/config.exs | 8 +++
.../docs/configuration/frontend_management.md | 14 ++++-
lib/pleroma/web/masto_fe_controller.ex | 14 ++++-
.../web/mastodon_api/views/status_view.ex | 4 +-
.../web/mastodon_api/websocket_handler.ex | 46 ++++++++++++--
.../controllers/emoji_reaction_controller.ex | 2 +
lib/pleroma/web/router.ex | 5 ++
lib/pleroma/web/streamer.ex | 15 +++--
.../masto_fe/fedibird.index.html.eex | 34 +++++++++++
...ndex.html.eex => glitchsoc.index.html.eex} | 0
lib/pleroma/web/views/masto_fe_view.ex | 8 ++-
lib/pleroma/web/views/streamer_view.ex | 15 +++--
.../integration/mastodon_websocket_test.exs | 4 +-
test/pleroma/notification_test.exs | 4 +-
test/pleroma/web/masto_fe_controller_test.exs | 38 ++++++++++++
.../mastodon_api/views/status_view_test.exs | 31 ++++++----
.../emoji_reaction_controller_test.exs | 11 +++-
test/pleroma/web/streamer_test.exs | 60 ++++++++++++-------
19 files changed, 256 insertions(+), 60 deletions(-)
create mode 100644 lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex
rename lib/pleroma/web/templates/masto_fe/{index.html.eex => glitchsoc.index.html.eex} (100%)
create mode 100644 test/pleroma/web/masto_fe_controller_test.exs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1e1d01c4..b32ae2e16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Added
+- support for fedibird-fe, and non-breaking API parity for it to function
+
### Fixed
- Compatibility with latest meilisearch
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
diff --git a/config/config.exs b/config/config.exs
index f49ec861c..83977da19 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -734,6 +734,14 @@
"build_dir" => "distribution",
"ref" => "akkoma"
},
+ "fedibird-fe" => %{
+ "name" => "fedibird-fe",
+ "git" => "https://akkoma.dev/AkkomaGang/fedibird-fe",
+ "build_url" =>
+ "https://akkoma-updates.s3-website.fr-par.scw.cloud/frontend/${ref}/fedibird-fe.zip",
+ "build_dir" => "distribution",
+ "ref" => "akkoma"
+ },
"admin-fe" => %{
"name" => "admin-fe",
"git" => "https://akkoma.dev/AkkomaGang/admin-fe",
diff --git a/docs/docs/configuration/frontend_management.md b/docs/docs/configuration/frontend_management.md
index a25120589..5e4b9b051 100644
--- a/docs/docs/configuration/frontend_management.md
+++ b/docs/docs/configuration/frontend_management.md
@@ -19,6 +19,10 @@ config :pleroma, :frontends,
admin: %{
"name" => "admin-fe",
"ref" => "stable"
+ },
+ mastodon: %{
+ "name" => "mastodon-fe",
+ "ref" => "akkoma"
}
```
@@ -26,12 +30,18 @@ This would serve the frontend from the the folder at `$instance_static/frontends
Refer to [the frontend CLI task](../../administration/CLI_tasks/frontend) for how to install the frontend's files
-If you wish masto-fe to also be enabled, you will also need to run the install task for `mastodon-fe`. Not doing this will lead to the frontend not working.
-
If you choose not to install a frontend for whatever reason, it is recommended that you enable [`:static_fe`](#static_fe) to allow remote users to click "view remote source". Don't bother with this if you've got no unauthenticated access though.
You can also replace the default "no frontend" page by placing an `index.html` file under your `instance/static/` directory.
+## Mastodon-FE
+
+Akkoma supports both [glitchsoc](https://github.com/glitch-soc/mastodon)'s more "vanilla" mastodon frontend,
+as well as [fedibird](https://github.com/fedibird/mastodon)'s extended frontend which has near-feature-parity with akkoma (with quoting and reactions).
+
+To enable either one, you must run the `frontend.install` task for either `mastodon-fe` or `fedibird-fe` (both `--ref akkoma`), then make sure
+`:pleroma, :frontends, :mastodon` references the one you want.
+
## Swagger (openAPI) documentation viewer
If you're a developer and you'd like a human-readable rendering of the
diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex
index d2460f51d..7b6e01aad 100644
--- a/lib/pleroma/web/masto_fe_controller.ex
+++ b/lib/pleroma/web/masto_fe_controller.ex
@@ -27,9 +27,21 @@ defmodule Pleroma.Web.MastoFEController do
def index(conn, _params) do
with %{assigns: %{user: %User{} = user, token: %Token{app_id: token_app_id} = token}} <- conn,
{:ok, %{id: ^token_app_id}} <- AuthController.local_mastofe_app() do
+ flavour =
+ [:frontends, :mastodon]
+ |> Pleroma.Config.get()
+ |> Map.get("name", "mastodon-fe")
+
+ index =
+ if flavour == "fedibird-fe" do
+ "fedibird.index.html"
+ else
+ "glitchsoc.index.html"
+ end
+
conn
|> put_layout(false)
- |> render("index.html",
+ |> render(index,
token: token.token,
user: user,
custom_emojis: Pleroma.Emoji.get_all()
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index f0fe9a4ba..d099c4901 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -375,6 +375,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
emojis: build_emojis(object.data["emoji"]),
quote_id: if(quote, do: quote.id, else: nil),
quote: maybe_render_quote(quote, opts),
+ emoji_reactions: emoji_reactions,
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
@@ -589,7 +590,8 @@ defp build_emoji_map(emoji, users, url, current_user) do
name: emoji,
count: length(users),
url: MediaProxy.url(url),
- me: !!(current_user && current_user.ap_id in users)
+ me: !!(current_user && current_user.ap_id in users),
+ account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
}
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 861a7ce3e..582e65d70 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -32,8 +32,15 @@ def init(%{qs: qs} = req, state) do
req
end
- {:cowboy_websocket, req, %{user: user, topic: topic, count: 0, timer: nil},
- %{idle_timeout: @timeout}}
+ {:cowboy_websocket, req,
+ %{
+ user: user,
+ topic: topic,
+ count: 0,
+ timer: nil,
+ subscriptions: [],
+ oauth_token: oauth_token
+ }, %{idle_timeout: @timeout}}
else
{:error, :bad_topic} ->
Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
@@ -65,21 +72,50 @@ def websocket_handle(:pong, state) do
# We only receive pings for now
def websocket_handle(:ping, state), do: {:ok, state}
- def websocket_handle({:text, "ping"}, state) do
+ def websocket_handle({:text, ping}, state) when ping in ~w[ping PING] do
if state.timer, do: Process.cancel_timer(state.timer)
{:reply, {:text, "pong"}, %{state | timer: timer()}}
end
+ def websocket_handle({:text, text}, state) do
+ with {:ok, json} <- Jason.decode(text) do
+ websocket_handle({:json, json}, state)
+ else
+ _ ->
+ Logger.error("#{__MODULE__} received text frame: #{text}")
+ {:ok, state}
+ end
+ end
+
+ def websocket_handle(
+ {:json, %{"type" => "subscribe", "stream" => stream_name}},
+ %{user: user, oauth_token: token} = state
+ ) do
+ with {:ok, topic} <- Streamer.get_topic(stream_name, user, token, %{}) do
+ new_subscriptions =
+ [topic | Map.get(state, :subscriptions, [])]
+ |> Enum.uniq()
+
+ {:ok, _topic} = Streamer.add_socket(topic, user)
+
+ {:ok, Map.put(state, :subscriptions, new_subscriptions)}
+ else
+ _ ->
+ Logger.error("#{__MODULE__} received invalid topic: #{stream_name}")
+ {:ok, state}
+ end
+ end
+
def websocket_handle(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
- def websocket_info({:render_with_user, view, template, item}, state) do
+ def websocket_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
- websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
+ websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
else
{:ok, state}
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
index 1de02faf8..91658587a 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -74,6 +74,8 @@ defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
defp filter(reactions, _), do: reactions
def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ emoji = Pleroma.Emoji.maybe_quote(emoji)
+
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index a0310bbb5..647d99278 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -457,6 +457,11 @@ defmodule Pleroma.Web.Router do
get("/federation_status", InstancesController, :show)
end
+ scope "/api/v1", Pleroma.Web.PleromaAPI do
+ pipe_through(:authenticated_api)
+ put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
+ end
+
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 9a4ac1317..d5b1d0678 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -114,6 +114,11 @@ def get_topic("list", _user, _oauth_token, _params) do
{:error, :unauthorized}
end
+ # mastodon multi-topic WS
+ def get_topic(nil, _user, _oauth_token, _params) do
+ {:ok, :multi}
+ end
+
def get_topic(_stream, _user, _oauth_token, _params) do
{:error, :bad_topic}
end
@@ -186,8 +191,8 @@ defp do_stream("direct", item) do
end
defp do_stream("follow_relationship", item) do
- text = StreamerView.render("follow_relationships_update.json", item)
user_topic = "user:#{item.follower.id}"
+ text = StreamerView.render("follow_relationships_update.json", item, user_topic)
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
@@ -235,7 +240,7 @@ defp do_stream(topic, %Notification{} = item)
when topic in ["user", "user:notification"] do
Registry.dispatch(@registry, "#{topic}:#{item.user_id}", fn list ->
Enum.each(list, fn {pid, _auth} ->
- send(pid, {:render_with_user, StreamerView, "notification.json", item})
+ send(pid, {:render_with_user, StreamerView, "notification.json", item, topic})
end)
end)
end
@@ -259,7 +264,7 @@ defp do_stream(topic, item) do
end
defp push_to_socket(topic, %Participation{} = participation) do
- rendered = StreamerView.render("conversation.json", participation)
+ rendered = StreamerView.render("conversation.json", participation, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _} ->
@@ -283,12 +288,12 @@ defp push_to_socket(topic, %Activity{
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
defp push_to_socket(topic, item) do
- anon_render = StreamerView.render("update.json", item)
+ anon_render = StreamerView.render("update.json", item, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
- send(pid, {:render_with_user, StreamerView, "update.json", item})
+ send(pid, {:render_with_user, StreamerView, "update.json", item, topic})
else
send(pid, {:text, anon_render})
end
diff --git a/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex b/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex
new file mode 100644
index 000000000..02c421831
--- /dev/null
+++ b/lib/pleroma/web/templates/masto_fe/fedibird.index.html.eex
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+<%= Config.get([:instance, :name]) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/pleroma/web/templates/masto_fe/index.html.eex b/lib/pleroma/web/templates/masto_fe/glitchsoc.index.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/masto_fe/index.html.eex
rename to lib/pleroma/web/templates/masto_fe/glitchsoc.index.html.eex
diff --git a/lib/pleroma/web/views/masto_fe_view.ex b/lib/pleroma/web/views/masto_fe_view.ex
index 63a9c8179..305368c9d 100644
--- a/lib/pleroma/web/views/masto_fe_view.ex
+++ b/lib/pleroma/web/views/masto_fe_view.ex
@@ -14,6 +14,7 @@ def initial_state(token, user, custom_emojis) do
%{
meta: %{
+ title: Config.get([:instance, :name]),
streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
access_token: token,
locale: "en",
@@ -27,7 +28,11 @@ def initial_state(token, user, custom_emojis) do
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit,
- mascot: User.get_mascot(user)["url"]
+ mascot: User.get_mascot(user)["url"],
+ show_quote_button: true,
+ enable_reaction: true,
+ compact_reaction: false,
+ advanced_layout: true
},
poll_limits: Config.get([:instance, :poll_limits]),
rights: %{
@@ -56,6 +61,7 @@ def initial_state(token, user, custom_emojis) do
"video\/mp4"
]
},
+ lists: [],
settings: user.mastofe_settings || %{},
push_subscription: nil,
accounts: %{user.id => render(AccountView, "show.json", user: user, for: user)},
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index de2e4d1e9..f455f941e 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -11,8 +11,9 @@ defmodule Pleroma.Web.StreamerView do
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.NotificationView
- def render("update.json", %Activity{} = activity, %User{} = user) do
+ def render("update.json", %Activity{} = activity, %User{} = user, topic) do
%{
+ stream: [topic],
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -25,8 +26,9 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!()
end
- def render("notification.json", %Notification{} = notify, %User{} = user) do
+ def render("notification.json", %Notification{} = notify, %User{} = user, topic) do
%{
+ stream: [topic],
event: "notification",
payload:
NotificationView.render(
@@ -38,8 +40,9 @@ def render("notification.json", %Notification{} = notify, %User{} = user) do
|> Jason.encode!()
end
- def render("update.json", %Activity{} = activity) do
+ def render("update.json", %Activity{} = activity, topic) do
%{
+ stream: [topic],
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
@@ -51,8 +54,9 @@ def render("update.json", %Activity{} = activity) do
|> Jason.encode!()
end
- def render("follow_relationships_update.json", item) do
+ def render("follow_relationships_update.json", item, topic) do
%{
+ stream: [topic],
event: "pleroma:follow_relationships_update",
payload:
%{
@@ -73,8 +77,9 @@ def render("follow_relationships_update.json", item) do
|> Jason.encode!()
end
- def render("conversation.json", %Participation{} = participation) do
+ def render("conversation.json", %Participation{} = participation, topic) do
%{
+ stream: [topic],
event: "conversation",
payload:
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 43ec57893..356bfa48d 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -31,9 +31,9 @@ def start_socket(qs \\ nil, headers \\ []) do
WebsocketClient.start_link(self(), path, headers)
end
- test "refuses invalid requests" do
+ test "allows multi-streams" do
capture_log(fn ->
- assert {:error, {404, _}} = start_socket()
+ assert {:ok, _} = start_socket()
assert {:error, {404, _}} = start_socket("?stream=ncjdk")
Process.sleep(30)
end)
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
index b47edd0a3..4354dd2b6 100644
--- a/test/pleroma/notification_test.exs
+++ b/test/pleroma/notification_test.exs
@@ -224,7 +224,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
task =
Task.async(fn ->
{:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, "user"}, 4_000
end)
task_user_notification =
@@ -232,7 +232,7 @@ test "it creates a notification for user and send to the 'user' and the 'user:no
{:ok, _topic} =
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
- assert_receive {:render_with_user, _, _, _}, 4_000
+ assert_receive {:render_with_user, _, _, _, "user:notification"}, 4_000
end)
activity = insert(:note_activity)
diff --git a/test/pleroma/web/masto_fe_controller_test.exs b/test/pleroma/web/masto_fe_controller_test.exs
new file mode 100644
index 000000000..924b45352
--- /dev/null
+++ b/test/pleroma/web/masto_fe_controller_test.exs
@@ -0,0 +1,38 @@
+defmodule Pleroma.Web.MastoFEControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Web.MastodonAPI.AuthController
+
+ describe "index/2 (main page)" do
+ test "GET /web/ (glitch-soc)" do
+ clear_config([:frontends, :mastodon], %{"name" => "mastodon-fe"})
+
+ {:ok, masto_app} = AuthController.local_mastofe_app()
+ user = Pleroma.Factory.insert(:user)
+ token = Pleroma.Factory.insert(:oauth_token, app: masto_app, user: user)
+ %{conn: conn} = oauth_access(["read", "write"], oauth_token: token, user: user)
+
+ resp =
+ conn
+ |> get("/web/getting-started")
+ |> html_response(200)
+
+ assert resp =~ "glitch"
+ end
+
+ test "GET /web/ (fedibird)" do
+ clear_config([:frontends, :mastodon], %{"name" => "fedibird-fe"})
+
+ {:ok, masto_app} = AuthController.local_mastofe_app()
+ user = Pleroma.Factory.insert(:user)
+ token = Pleroma.Factory.insert(:oauth_token, app: masto_app, user: user)
+ %{conn: conn} = oauth_access(["read", "write"], oauth_token: token, user: user)
+
+ resp =
+ conn
+ |> get("/web/getting-started")
+ |> html_response(200)
+
+ refute resp =~ "glitch"
+ end
+ end
+end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index fb3255927..a6f8f3fc8 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -44,14 +44,15 @@ test "has an emoji reaction list" do
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: false, url: nil},
+ %{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
%{
count: 2,
me: false,
name: "dinosaur",
- url: "http://localhost:4001/emoji/dino walking.gif"
+ url: "http://localhost:4001/emoji/dino walking.gif",
+ account_ids: [other_user.id, user.id]
},
- %{name: "🍵", count: 1, me: false, url: nil}
+ %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
@@ -59,14 +60,15 @@ test "has an emoji reaction list" do
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: true, url: nil},
+ %{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
%{
count: 2,
me: true,
name: "dinosaur",
- url: "http://localhost:4001/emoji/dino walking.gif"
+ url: "http://localhost:4001/emoji/dino walking.gif",
+ account_ids: [other_user.id, user.id]
},
- %{name: "🍵", count: 1, me: false, url: nil}
+ %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
end
@@ -82,7 +84,7 @@ test "works correctly with badly formatted emojis" do
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: true, url: nil}
+ %{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]}
]
end
@@ -102,7 +104,7 @@ test "doesn't show reactions from muted and blocked users" do
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: false, url: nil}
+ %{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
@@ -114,19 +116,25 @@ test "doesn't show reactions from muted and blocked users" do
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 2, me: false, url: nil}
+ %{
+ name: "☕",
+ count: 2,
+ me: false,
+ url: nil,
+ account_ids: [third_user.id, other_user.id]
+ }
]
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: false, url: nil}
+ %{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: other_user)
assert status[:pleroma][:emoji_reactions] == [
- %{name: "☕", count: 1, me: true, url: nil}
+ %{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]}
]
end
@@ -272,6 +280,7 @@ test "a note activity" do
spoiler_text: HTML.filter_tags(object_data["summary"]),
visibility: "public",
media_attachments: [],
+ emoji_reactions: [],
mentions: [],
tags: [
%{
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
index 65bb22e27..4898179e6 100644
--- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
@@ -31,7 +31,13 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert to_string(activity.id) == id
assert result["pleroma"]["emoji_reactions"] == [
- %{"name" => "☕", "count" => 1, "me" => true, "url" => nil}
+ %{
+ "name" => "☕",
+ "count" => 1,
+ "me" => true,
+ "url" => nil,
+ "account_ids" => [other_user.id]
+ }
]
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
@@ -54,7 +60,8 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
"name" => "dinosaur",
"count" => 1,
"me" => true,
- "url" => "http://localhost:4001/emoji/dino walking.gif"
+ "url" => "http://localhost:4001/emoji/dino walking.gif",
+ "account_ids" => [other_user.id]
}
]
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 841db0e91..07129ff11 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -157,7 +157,8 @@ test "it streams the user's post in the 'user' stream", %{user: user, token: oau
Streamer.get_topic_and_add_socket("user", user, oauth_token)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
- assert_receive {:render_with_user, _, _, ^activity}
+ stream_name = "user:#{user.id}"
+ assert_receive {:render_with_user, _, _, ^activity, ^stream_name}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -168,7 +169,11 @@ test "it streams boosts of the user in the 'user' stream", %{user: user, token:
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
{:ok, announce} = CommonAPI.repeat(activity.id, user)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ stream_name = "user:#{user.id}"
+
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce,
+ ^stream_name}
+
refute Streamer.filtered_by_user?(user, announce)
end
@@ -221,7 +226,11 @@ test "it streams boosts of mastodon user in the 'user' stream", %{
{:ok, %Pleroma.Activity{data: _data, local: false} = announce} =
Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data)
- assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce}
+ stream_name = "user:#{user.id}"
+
+ assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce,
+ ^stream_name}
+
refute Streamer.filtered_by_user?(user, announce)
end
@@ -233,7 +242,7 @@ test "it sends notify to in the 'user' stream", %{
Streamer.get_topic_and_add_socket("user", user, oauth_token)
Streamer.stream("user", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, "user"}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -245,7 +254,7 @@ test "it sends notify to in the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
Streamer.stream("user:notification", notify)
- assert_receive {:render_with_user, _, _, ^notify}
+ assert_receive {:render_with_user, _, _, ^notify, "user:notification"}
refute Streamer.filtered_by_user?(user, notify)
end
@@ -291,7 +300,7 @@ test "it sends favorite to 'user:notification' stream'", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, "user:notification"}
assert notif.activity.id == favorite_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -320,7 +329,7 @@ test "it sends follow activities to the 'user:notification' stream", %{
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
{:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, "user:notification"}
assert notif.activity.id == follow_activity.id
refute Streamer.filtered_by_user?(user, notif)
end
@@ -384,7 +393,7 @@ test "it sends to public (authenticated)" do
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(other_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, "public"}
refute Streamer.filtered_by_user?(other_user, activity)
end
@@ -436,7 +445,7 @@ test "it filters to user if recipients invalid and thread containment is enabled
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, "public"}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -458,7 +467,7 @@ test "it sends message if recipients invalid and thread containment is disabled"
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, "public"}
refute Streamer.filtered_by_user?(user, activity)
end
@@ -481,7 +490,7 @@ test "it sends message if recipients invalid and thread containment is enabled b
Streamer.get_topic_and_add_socket("public", user, oauth_token)
Streamer.stream("public", activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, "public"}
refute Streamer.filtered_by_user?(user, activity)
end
end
@@ -495,7 +504,7 @@ test "it filters messages involving blocked users", %{user: user, token: oauth_t
Streamer.get_topic_and_add_socket("public", user, oauth_token)
{:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"})
- assert_receive {:render_with_user, _, _, ^activity}
+ assert_receive {:render_with_user, _, _, ^activity, "public"}
assert Streamer.filtered_by_user?(user, activity)
end
@@ -512,17 +521,17 @@ test "it filters messages transitively involving blocked users", %{
{:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_one}
+ assert_receive {:render_with_user, _, _, ^activity_one, "public"}
assert Streamer.filtered_by_user?(blocker, activity_one)
{:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_two}
+ assert_receive {:render_with_user, _, _, ^activity_two, "public"}
assert Streamer.filtered_by_user?(blocker, activity_two)
{:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"})
- assert_receive {:render_with_user, _, _, ^activity_three}
+ assert_receive {:render_with_user, _, _, ^activity_three, "public"}
assert Streamer.filtered_by_user?(blocker, activity_three)
end
end
@@ -583,7 +592,8 @@ test "it sends wanted private posts to list", %{user: user_a, token: user_a_toke
visibility: "private"
})
- assert_receive {:render_with_user, _, _, ^activity}
+ stream_name = "list:#{list.id}"
+ assert_receive {:render_with_user, _, _, ^activity, ^stream_name}
refute Streamer.filtered_by_user?(user_a, activity)
end
end
@@ -601,7 +611,8 @@ test "it filters muted reblogs", %{user: user1, token: user1_token} do
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, _, ^announce_activity}
+ stream_name = "user:#{user1.id}"
+ assert_receive {:render_with_user, _, _, ^announce_activity, ^stream_name}
assert Streamer.filtered_by_user?(user1, announce_activity)
end
@@ -617,7 +628,7 @@ test "it filters reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, "user"}
assert Streamer.filtered_by_user?(user1, notif)
end
@@ -633,7 +644,7 @@ test "it send non-reblog notification for reblog-muted actors", %{
Streamer.get_topic_and_add_socket("user", user1, user1_token)
{:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id)
- assert_receive {:render_with_user, _, "notification.json", notif}
+ assert_receive {:render_with_user, _, "notification.json", notif, "user"}
refute Streamer.filtered_by_user?(user1, notif)
end
end
@@ -648,7 +659,8 @@ test "it filters posts from muted threads" do
{:ok, activity} = CommonAPI.post(user, %{status: "super hot take"})
{:ok, _} = CommonAPI.add_mute(user2, activity)
- assert_receive {:render_with_user, _, _, ^activity}
+ stream_name = "user:#{user2.id}"
+ assert_receive {:render_with_user, _, _, ^activity, ^stream_name}
assert Streamer.filtered_by_user?(user2, activity)
end
end
@@ -690,7 +702,8 @@ test "it doesn't send conversation update to the 'direct' stream when the last m
})
create_activity_id = create_activity.id
- assert_receive {:render_with_user, _, _, ^create_activity}
+ stream_name = "direct:#{user.id}"
+ assert_receive {:render_with_user, _, _, ^create_activity, ^stream_name}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
@@ -725,8 +738,9 @@ test "it sends conversation update to the 'direct' stream when a message is dele
visibility: "direct"
})
- assert_receive {:render_with_user, _, _, ^create_activity}
- assert_receive {:render_with_user, _, _, ^create_activity2}
+ stream_name = "direct:#{user.id}"
+ assert_receive {:render_with_user, _, _, ^create_activity, ^stream_name}
+ assert_receive {:render_with_user, _, _, ^create_activity2, ^stream_name}
assert_receive {:text, received_conversation1}
assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1)
assert_receive {:text, received_conversation1}
From aaf78e2b52c3aa1e75206e7dbe41b45874978e0a Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Wed, 17 Aug 2022 09:35:11 +0000
Subject: [PATCH 05/44] only put linked mfm in source (#171)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/171
---
config/description.exs | 43 ++++++++++++++++---
.../article_note_page_validator.ex | 5 +--
test/fixtures/misskey/mfm_x_format.json | 2 +-
.../article_note_page_validator_test.exs | 13 +++---
4 files changed, 44 insertions(+), 19 deletions(-)
diff --git a/config/description.exs b/config/description.exs
index 9f93265d1..b70982cd2 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1169,7 +1169,6 @@
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
- hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.svg",
@@ -1235,12 +1234,6 @@
type: :boolean,
description: "Hide notices statistics (repeats, favorites, ...)"
},
- %{
- key: :hideSitename,
- label: "Hide Sitename",
- type: :boolean,
- description: "Hides instance name from PleromaFE banner"
- },
%{
key: :hideUserStats,
label: "Hide user stats",
@@ -1350,6 +1343,42 @@
type: :string,
description: "Which theme to use. Available themes are defined in styles.json",
suggestions: ["pleroma-dark"]
+ },
+ %{
+ key: :showPanelNavShortcuts,
+ label: "Show timeline panel nav shortcuts",
+ type: :boolean,
+ description: "Whether to put timeline nav tabs on the top of the panel"
+ },
+ %{
+ key: :showNavShortcuts,
+ label: "Show navbar shortcuts",
+ type: :boolean,
+ description: "Whether to put extra navigation options on the navbar"
+ },
+ %{
+ key: :showWiderShortcuts,
+ label: "Increase navbar shortcut spacing",
+ type: :boolean,
+ description: "Whether to add extra space between navbar icons"
+ },
+ %{
+ key: :hideSiteFavicon,
+ label: "Hide site favicon",
+ type: :boolean,
+ description: "Whether to hide the instance favicon from the navbar"
+ },
+ %{
+ key: :hideSiteName,
+ label: "Hide site name",
+ type: :boolean,
+ description: "Whether to hide the site name from the navbar"
+ },
+ %{
+ key: :renderMisskeyMarkdown,
+ label: "Render misskey markdown",
+ type: :boolean,
+ description: "Whether to render Misskey-flavoured markdown"
}
]
},
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index eee7629ad..f2779432e 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -119,7 +119,7 @@ defp fix_misskey_content(
{linked, _, _} =
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
- Map.put(object, "content", linked)
+ put_in(object, ["source", "content"], linked)
end
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
@@ -132,10 +132,9 @@ defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_bina
object
|> Map.put("source", %{
- "content" => content,
+ "content" => linked,
"mediaType" => "text/x.misskeymarkdown"
})
- |> Map.put("content", linked)
|> Map.delete("_misskey_content")
end
diff --git a/test/fixtures/misskey/mfm_x_format.json b/test/fixtures/misskey/mfm_x_format.json
index 21aae9204..590e399fe 100644
--- a/test/fixtures/misskey/mfm_x_format.json
+++ b/test/fixtures/misskey/mfm_x_format.json
@@ -3,7 +3,7 @@
"type": "Note",
"attributedTo": "https://misskey.local.live/users/92hzkskwgy",
"summary": null,
- "content": "this gets replaced",
+ "content": "this does not get replaced",
"source": {
"content": "@akkoma_user @remote_user @full_tag_remote_user@misskey.local.live @oops_not_a_mention linkifylink #dancedance $[jelly mfm goes here] \n\n## aaa",
"mediaType": "text/x.misskeymarkdown"
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index 09cd1a964..f419770f2 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -96,10 +96,9 @@ test "a misskey MFM status with a content field should work and be linked", _ do
%{
valid?: true,
changes: %{
- content: content,
+ content: "this does not get replaced",
source: %{
- "content" =>
- "@akkoma_user @remote_user @full_tag_remote_user@misskey.local.live @oops_not_a_mention linkifylink #dancedance $[jelly mfm goes here] \n\n## aaa",
+ "content" => content,
"mediaType" => "text/x.misskeymarkdown"
}
}
@@ -129,22 +128,20 @@ test "a misskey MFM status with a _misskey_content field should work and be link
|> File.read!()
|> Jason.decode!()
- expected_content =
- "@akkoma_user linkifylink #dancedance $[jelly mfm goes here] ## aaa"
-
changes = ArticleNotePageValidator.cast_and_validate(note)
%{
valid?: true,
changes: %{
source: %{
- "content" => "@akkoma_user linkifylink #dancedance $[jelly mfm goes here] \n\n## aaa",
+ "content" => content,
"mediaType" => "text/x.misskeymarkdown"
}
}
} = changes
- assert changes.changes[:content] == expected_content
+ assert content =~
+ "@akkoma_user "
end
end
end
From e9f1897cfdb32c890e9eaf2e894128be5c7e1123 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Thu, 18 Aug 2022 03:14:48 +0000
Subject: [PATCH 06/44] parser MFM server-side (#172)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/172
---
.../article_note_page_validator.ex | 7 ++--
lib/pleroma/web/common_api/utils.ex | 8 ++---
mix.exs | 5 ++-
mix.lock | 1 +
priv/scrubbers/default.ex | 34 +++++++++++++++++--
.../article_note_page_validator_test.exs | 12 ++++---
6 files changed, 53 insertions(+), 14 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index f2779432e..28053ea3a 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -108,6 +108,8 @@ defp remote_mention_resolver(
end
# https://github.com/misskey-dev/misskey/pull/8787
+ # Misskey has an awful tendency to drop all custom formatting when it sends remotely
+ # So this basically reprocesses their MFM source
defp fix_misskey_content(
%{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
)
@@ -119,7 +121,7 @@ defp fix_misskey_content(
{linked, _, _} =
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
- put_in(object, ["source", "content"], linked)
+ Map.put(object, "content", linked)
end
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
@@ -132,9 +134,10 @@ defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_bina
object
|> Map.put("source", %{
- "content" => linked,
+ "content" => content,
"mediaType" => "text/x.misskeymarkdown"
})
+ |> Map.put("content", linked)
|> Map.delete("_misskey_content")
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 61af71acd..15016eb47 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -285,11 +285,11 @@ def format_input(text, "text/html", options) do
def format_input(text, "text/x.misskeymarkdown", options) do
text
+ |> Formatter.markdown_to_html()
+ |> MfmParser.Parser.parse()
+ |> MfmParser.Encoder.to_html()
|> Formatter.linkify(options)
- |> Formatter.html_escape("text/x.misskeymarkdown")
- |> (fn {text, mentions, tags} ->
- {String.replace(text, ~r/\r?\n/, " "), mentions, tags}
- end).()
+ |> Formatter.html_escape("text/html")
end
def format_input(text, "text/markdown", options) do
diff --git a/mix.exs b/mix.exs
index c6bd0e28f..e7f491997 100644
--- a/mix.exs
+++ b/mix.exs
@@ -129,7 +129,7 @@ defp deps do
override: true},
{:bcrypt_elixir, "~> 2.2"},
{:trailing_format_plug, "~> 0.0.7"},
- {:fast_sanitize, "~> 0.2.0"},
+ {:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
{:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"},
@@ -191,6 +191,9 @@ defp deps do
{:ecto_psql_extras, "~> 0.6"},
{:elasticsearch,
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
+ {:mfm_parser,
+ git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
+ ref: "5054e0ba1ebcbd9a7916aec219528e3e58057241"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
diff --git a/mix.lock b/mix.lock
index 2d3c9f33e..1c3b550e7 100644
--- a/mix.lock
+++ b/mix.lock
@@ -67,6 +67,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "5054e0ba1ebcbd9a7916aec219528e3e58057241", [ref: "5054e0ba1ebcbd9a7916aec219528e3e58057241"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 153b0be45..68ac06e32 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -56,8 +56,36 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:u, [])
Meta.allow_tag_with_these_attributes(:ul, [])
- Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "quote-inline"])
- Meta.allow_tag_with_these_attributes(:span, [])
+ Meta.allow_tags_with_style_attributes([:span])
+
+ Meta.allow_tag_with_this_attribute_values(:span, "class", [
+ "h-card",
+ "quote-inline",
+ "mfm",
+ "_mfm_tada_",
+ "_mfm_jelly_",
+ "_mfm_twitch_",
+ "_mfm_shake_",
+ "_mfm_spin_",
+ "_mfm_jump_",
+ "_mfm_bounce_",
+ "_mfm_flip_",
+ "_mfm_x2_",
+ "_mfm_x3_",
+ "_mfm_x4_",
+ "_mfm_blur_",
+ "_mfm_rainbow_",
+ "_mfm_rotate_"
+ ])
+
+ Meta.allow_tag_with_these_attributes(:span, [
+ "data-x",
+ "data-y",
+ "data-h",
+ "data-v",
+ "data-left",
+ "data-right"
+ ])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])
@@ -101,4 +129,6 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes(:small, [])
Meta.strip_everything_not_covered()
+
+ defp scrub_css(value), do: value
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index f419770f2..c766414a6 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -96,9 +96,8 @@ test "a misskey MFM status with a content field should work and be linked", _ do
%{
valid?: true,
changes: %{
- content: "this does not get replaced",
+ content: content,
source: %{
- "content" => content,
"mediaType" => "text/x.misskeymarkdown"
}
}
@@ -114,7 +113,9 @@ test "a misskey MFM status with a content field should work and be linked", _ do
"@full_tag_remote_user "
assert content =~ "@oops_not_a_mention"
- assert content =~ "$[jelly mfm goes here] ## aaa"
+
+ assert content =~
+ "mfm goes here
aaa"
end
test "a misskey MFM status with a _misskey_content field should work and be linked", _ do
@@ -133,9 +134,10 @@ test "a misskey MFM status with a _misskey_content field should work and be link
%{
valid?: true,
changes: %{
+ content: content,
source: %{
- "content" => content,
- "mediaType" => "text/x.misskeymarkdown"
+ "mediaType" => "text/x.misskeymarkdown",
+ "content" => "@akkoma_user linkifylink #dancedance $[jelly mfm goes here] \n\n## aaa"
}
}
} = changes
From a8f8ecce31238309e677802ad57d6521d58b814d Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Thu, 18 Aug 2022 04:23:07 +0100
Subject: [PATCH 07/44] add changelog entry
---
CHANGELOG.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b32ae2e16..41b352f21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- support for fedibird-fe, and non-breaking API parity for it to function
+### Changed
+- MFM parsing is now done on the backend by a modified version of ilja's parser -> https://akkoma.dev/AkkomaGang/mfm-parser
+
### Fixed
- Compatibility with latest meilisearch
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
From 22333f13e834f163f32574e89aed4d3bed5f0684 Mon Sep 17 00:00:00 2001
From: Karen Konou
Date: Thu, 18 Aug 2022 12:43:20 +0200
Subject: [PATCH 08/44] Update OTP systemd service
---
.../{pleroma.service => akkoma.service} | 22 +++++++++----------
1 file changed, 11 insertions(+), 11 deletions(-)
rename rel/files/installation/{pleroma.service => akkoma.service} (63%)
diff --git a/rel/files/installation/pleroma.service b/rel/files/installation/akkoma.service
similarity index 63%
rename from rel/files/installation/pleroma.service
rename to rel/files/installation/akkoma.service
index e47cf58dc..0719dbf73 100644
--- a/rel/files/installation/pleroma.service
+++ b/rel/files/installation/akkoma.service
@@ -1,27 +1,27 @@
[Unit]
-Description=Pleroma social network
+Description=Akkoma social network
After=network.target postgresql.service nginx.service
[Service]
KillMode=process
Restart=on-failure
-; Name of the user that runs the Pleroma service.
-User=pleroma
+; Name of the user that runs the Akkoma service.
+User=akkoma
; Make sure that all paths fit your installation.
-; Path to the home directory of the user running the Pleroma service.
-Environment="HOME=/opt/pleroma"
-; Path to the folder containing the Pleroma installation.
-WorkingDirectory=/opt/pleroma
-; Path to the Pleroma binary.
-ExecStart=/opt/pleroma/bin/pleroma start
-ExecStop=/opt/pleroma/bin/pleroma stop
+; Path to the home directory of the user running the Akkoma service.
+Environment="HOME=/opt/akkoma"
+; Path to the folder containing the Akkoma installation.
+WorkingDirectory=/opt/akkoma
+; Path to the Mix binary.
+ExecStart=/opt/akkoma/bin/pleroma start
+ExecStop=/opt/akkoma/bin/pleroma stop
; Some security directives.
; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
PrivateTmp=true
-; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Pleroma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
+; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Akkoma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
ProtectHome=true
; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
ProtectSystem=full
From ffbf8304e057631f4d8bdf56cb87f8bacafb751e Mon Sep 17 00:00:00 2001
From: Norm
Date: Thu, 18 Aug 2022 23:13:09 +0000
Subject: [PATCH 09/44] Update OTP OpenRC service
This makes the paths match that of the OTP install guide on OpenRC distros.
---
rel/files/installation/init.d/{pleroma => akkoma} | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
rename rel/files/installation/init.d/{pleroma => akkoma} (70%)
diff --git a/rel/files/installation/init.d/pleroma b/rel/files/installation/init.d/akkoma
similarity index 70%
rename from rel/files/installation/init.d/pleroma
rename to rel/files/installation/init.d/akkoma
index dea1db26c..ea6ea3580 100755
--- a/rel/files/installation/init.d/pleroma
+++ b/rel/files/installation/init.d/akkoma
@@ -3,17 +3,17 @@
supervisor=supervise-daemon
# Requires OpenRC >= 0.35
-directory=/opt/pleroma
+directory=/opt/akkoma
-command=/opt/pleroma/bin/pleroma
+command=/opt/akkoma/bin/pleroma
command_args="start"
-command_user=pleroma
+command_user=akkoma
command_background=1
# Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30/SIGKILL/5"
-pidfile="/var/run/pleroma.pid"
+pidfile="/var/run/akkoma.pid"
depend() {
want nginx
From 429e2ac832a874ae8ba8a9c116da61a6273c8a87 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Sun, 21 Aug 2022 14:46:52 +0000
Subject: [PATCH 10/44] oauth2 fixes (#177)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/177
---
lib/pleroma/helpers/auth_helper.ex | 18 -----
.../controllers/auth_controller.ex | 11 +--
lib/pleroma/web/o_auth/o_auth_controller.ex | 77 ++++++++++---------
lib/pleroma/web/o_auth/token.ex | 6 ++
lib/pleroma/web/o_auth/token/query.ex | 8 +-
lib/pleroma/web/plugs/o_auth_plug.ex | 13 +---
.../web/plugs/set_user_session_id_plug.ex | 18 -----
lib/pleroma/web/router.ex | 5 +-
mix.exs | 4 +-
.../web/o_auth/o_auth_controller_test.exs | 53 ++-----------
test/pleroma/web/plugs/o_auth_plug_test.exs | 56 --------------
.../plugs/set_user_session_id_plug_test.exs | 43 -----------
12 files changed, 69 insertions(+), 243 deletions(-)
delete mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex
delete mode 100644 test/pleroma/web/plugs/set_user_session_id_plug_test.exs
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index 13e4c8158..d56f6f461 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -4,12 +4,9 @@
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
- alias Plug.Conn
import Plug.Conn
- @oauth_token_session_key :oauth_token
-
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
@@ -28,19 +25,4 @@ def drop_auth_info(conn) do
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
-
- @doc "Gets OAuth token string from session"
- def get_session_token(%Conn{} = conn) do
- get_session(conn, @oauth_token_session_key)
- end
-
- @doc "Updates OAuth token string in session"
- def put_session_token(%Conn{} = conn, token) when is_binary(token) do
- put_session(conn, @oauth_token_session_key, token)
- end
-
- @doc "Deletes OAuth token string from session"
- def delete_session_token(%Conn{} = conn) do
- delete_session(conn, @oauth_token_session_key)
- end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 4920d65da..f415e5931 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -34,7 +33,6 @@ def login(conn, %{"code" => auth_token} = params) do
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
- |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
@@ -42,9 +40,9 @@ def login(conn, %{"code" => auth_token} = params) do
end
def login(conn, params) do
- with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id, token: token}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
- redirect(conn, to: local_mastodon_post_login_path(conn))
+ redirect(conn, to: local_mastodon_post_login_path(conn) <> "?access_token=#{token}")
else
_ -> redirect_to_oauth_form(conn, params)
end
@@ -68,9 +66,8 @@ defp redirect_to_oauth_form(conn, _params) do
def logout(conn, _) do
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
- session_token = AuthHelper.get_session_token(conn),
- {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
- AuthHelper.delete_session_token(conn)
+ {:ok, %Token{token: _session_token}} <- RevokeToken.revoke(oauth_token) do
+ conn
else
_ -> conn
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 358120fe6..43536f95d 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@@ -77,33 +76,46 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
- user =
- with %{assigns: %{user: %User{} = user}} <- conn do
- user
- else
- _ -> nil
- end
+ # if we already have a token for this specific setup, we can use that
+ with false <- Params.truthy_param?(params["force_login"]),
+ %App{} <- app,
+ {:ok, _} <- Scopes.validate(scopes, app.scopes),
+ {:ok, %Token{} = token} <- Token.get_by_app(app) do
+ token = Repo.preload(token, :app)
- scopes =
- if scopes == [] do
- available_scopes
- else
- scopes
- end
+ conn
+ |> assign(:token, token)
+ |> handle_existing_authorization(params)
+ else
+ _ ->
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
- # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
- render(conn, Authenticator.auth_template(), %{
- user: user,
- app: app && Map.delete(app, :client_secret),
- response_type: params["response_type"],
- client_id: params["client_id"],
- available_scopes: available_scopes,
- scopes: scopes,
- redirect_uri: params["redirect_uri"],
- state: params["state"],
- params: params,
- view_module: OAuthView
- })
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
+
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+ render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
+ response_type: params["response_type"],
+ client_id: params["client_id"],
+ available_scopes: available_scopes,
+ scopes: scopes,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params,
+ view_module: OAuthView
+ })
+ end
end
defp handle_existing_authorization(
@@ -318,9 +330,8 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
- def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
conn
- |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
@@ -379,15 +390,7 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
- {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
- conn =
- with session_token = AuthHelper.get_session_token(conn),
- %Token{token: ^session_token} <- oauth_token do
- AuthHelper.delete_session_token(conn)
- else
- _ -> conn
- end
-
+ {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
json(conn, %{})
else
_error ->
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 9d69e9db4..08c8cd298 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -39,6 +39,12 @@ def get_by_token(token) do
|> Repo.find_resource()
end
+ def get_by_app(%App{} = app) do
+ app.id
+ |> Query.get_unexpired_by_app()
+ |> Repo.find_resource()
+ end
+
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index d16a759d8..8edfcf5d7 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -23,9 +23,15 @@ def get_by_token(query \\ Token, token) do
from(q in query, where: q.token == ^token)
end
+ @spec get_unexpired_by_app(query, String.t()) :: query
+ def get_unexpired_by_app(query \\ Token, app_id) do
+ time = NaiveDateTime.utc_now()
+ from(q in query, where: q.app_id == ^app_id and q.valid_until > ^time, limit: 1)
+ end
+
@spec get_by_app(query, String.t()) :: query
def get_by_app(query \\ Token, app_id) do
- from(q in query, where: q.app_id == ^app_id)
+ from(q in query, where: q.app_id == ^app_id, limit: 1)
end
@spec get_by_id(query, String.t()) :: query
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 5e06ac3f6..29b3316b3 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -18,8 +17,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def init(options), do: options
- def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
-
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
@@ -82,7 +79,7 @@ defp fetch_token_str(%Plug.Conn{} = conn) do
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
- _ -> fetch_token_from_session(conn)
+ _ -> :no_token_found
end
end
@@ -96,12 +93,4 @@ defp fetch_token_str([token | tail]) do
end
defp fetch_token_str([]), do: :no_token_found
-
- @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
- defp fetch_token_from_session(conn) do
- case AuthHelper.get_session_token(conn) do
- nil -> :no_token_found
- token -> {:ok, token}
- end
- end
end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
deleted file mode 100644
index a1cfa0915..000000000
--- a/lib/pleroma/web/plugs/set_user_session_id_plug.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
- alias Pleroma.Helpers.AuthHelper
- alias Pleroma.Web.OAuth.Token
-
- def init(opts) do
- opts
- end
-
- def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
- AuthHelper.put_session_token(conn, oauth_token.token)
- end
-
- def call(conn, _), do: conn
-end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 647d99278..f2d6b0aff 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -57,7 +57,6 @@ defmodule Pleroma.Web.Router do
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
- plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
@@ -793,10 +792,8 @@ defmodule Pleroma.Web.Router do
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
-
- post("/auth/password", MastodonAPI.AuthController, :password_reset)
-
get("/web/*path", MastoFEController, :index)
+ post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/embed/:id", EmbedController, :show)
end
diff --git a/mix.exs b/mix.exs
index e7f491997..170276b0a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -131,7 +131,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.1", override: true},
+ {:phoenix_html, "~> 3.0", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
{:poison, "~> 3.0", override: true},
@@ -152,7 +152,7 @@ defp deps do
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
- {:swoosh, "~> 1.0"},
+ {:swoosh, "~> 1.3"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 0fdd5b8e9..4e197a485 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
@@ -456,7 +455,7 @@ test "renders authentication page if user is already authenticated but `force_lo
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -480,7 +479,7 @@ test "renders authentication page if user is already authenticated but user requ
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -503,7 +502,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -529,7 +528,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -553,7 +552,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -611,41 +610,6 @@ test "redirects with oauth authorization, " <>
end
end
- test "authorize from cookie" do
- user = insert(:user)
- app = insert(:oauth_app)
- oauth_token = insert(:oauth_token, user: user, app: app)
- redirect_uri = OAuthController.default_redirect_uri(app)
-
- conn =
- build_conn()
- |> Plug.Session.call(Plug.Session.init(@session_opts))
- |> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
- |> post(
- "/oauth/authorize",
- %{
- "authorization" => %{
- "name" => user.nickname,
- "client_id" => app.client_id,
- "redirect_uri" => redirect_uri,
- "scope" => app.scopes,
- "state" => "statepassed"
- }
- }
- )
-
- target = redirected_to(conn)
- assert target =~ redirect_uri
-
- query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
-
- assert %{"state" => "statepassed", "code" => code} = query
- auth = Repo.get_by(Authorization, token: code)
- assert auth
- assert auth.scopes == app.scopes
- end
-
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
@@ -1218,6 +1182,7 @@ test "issues a new token if token expired" do
response =
build_conn()
+ |> put_req_header("authorization", "Bearer #{access_token.token}")
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
@@ -1267,12 +1232,11 @@ test "when authenticated with request token, revokes it and clears it from sessi
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
+ |> put_req_header("authorization", "Bearer #{oauth_token.token}")
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
- refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
@@ -1286,12 +1250,11 @@ test "if request is authenticated with a different token, " <>
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
+ |> put_req_header("authorization", "Bearer #{oauth_token.token}")
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
- assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs
index 9e4be5559..caabfc1cb 100644
--- a/test/pleroma/web/plugs/o_auth_plug_test.exs
+++ b/test/pleroma/web/plugs/o_auth_plug_test.exs
@@ -5,11 +5,8 @@
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.OAuth.Token.Strategy.Revoke
alias Pleroma.Web.Plugs.OAuthPlug
- alias Plug.Session
import Pleroma.Factory
@@ -72,57 +69,4 @@ test "with invalid token, it does not assign the user", %{conn: conn} do
refute conn.assigns[:user]
end
-
- describe "with :oauth_token in session, " do
- setup %{token: oauth_token, conn: conn} do
- session_opts = [
- store: :cookie,
- key: "_test",
- signing_salt: "cooldude"
- ]
-
- conn =
- conn
- |> Session.call(Session.init(session_opts))
- |> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
-
- %{conn: conn}
- end
-
- test "if session-stored token matches a valid OAuth token, assigns :user and :token", %{
- conn: conn,
- user: user,
- token: oauth_token
- } do
- conn = OAuthPlug.call(conn, %{})
-
- assert conn.assigns.user && conn.assigns.user.id == user.id
- assert conn.assigns.token && conn.assigns.token.id == oauth_token.id
- end
-
- test "if session-stored token matches an expired OAuth token, does nothing", %{
- conn: conn,
- token: oauth_token
- } do
- expired_valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600 * 24, :second)
-
- oauth_token
- |> Ecto.Changeset.change(valid_until: expired_valid_until)
- |> Pleroma.Repo.update()
-
- ret_conn = OAuthPlug.call(conn, %{})
- assert ret_conn == conn
- end
-
- test "if session-stored token matches a revoked OAuth token, does nothing", %{
- conn: conn,
- token: oauth_token
- } do
- Revoke.revoke(oauth_token)
-
- ret_conn = OAuthPlug.call(conn, %{})
- assert ret_conn == conn
- end
- end
end
diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
deleted file mode 100644
index 9814c80d8..000000000
--- a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
+++ /dev/null
@@ -1,43 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
- use Pleroma.Web.ConnCase, async: true
-
- alias Pleroma.Helpers.AuthHelper
- alias Pleroma.Web.Plugs.SetUserSessionIdPlug
-
- setup %{conn: conn} do
- session_opts = [
- store: :cookie,
- key: "_test",
- signing_salt: "cooldude"
- ]
-
- conn =
- conn
- |> Plug.Session.call(Plug.Session.init(session_opts))
- |> fetch_session()
-
- %{conn: conn}
- end
-
- test "doesn't do anything if the user isn't set", %{conn: conn} do
- ret_conn = SetUserSessionIdPlug.call(conn, %{})
-
- assert ret_conn == conn
- end
-
- test "sets session token basing on :token assign", %{conn: conn} do
- %{user: user, token: oauth_token} = oauth_access(["read"])
-
- ret_conn =
- conn
- |> assign(:user, user)
- |> assign(:token, oauth_token)
- |> SetUserSessionIdPlug.call(%{})
-
- assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
- end
-end
From d72f9e39d9f76ee8bbd26c068b2870ea945705b7 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Sun, 21 Aug 2022 15:17:01 +0000
Subject: [PATCH 11/44] add visibility check on quote (#178)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/178
---
.../web/mastodon_api/views/status_view.ex | 10 +++++--
.../mastodon_api/views/status_view_test.exs | 28 +++++++++++++++++++
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index d099c4901..d838c4673 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -623,15 +623,19 @@ defp build_image_url(_, _), do: nil
defp maybe_render_quote(nil, _), do: nil
defp maybe_render_quote(quote, opts) do
- if opts[:do_not_recurse] || !visible_for_user?(quote, opts[:for]) do
- nil
- else
+ with %User{} = quoted_user <- User.get_cached_by_ap_id(quote.actor),
+ false <- Map.get(opts, :do_not_recurse, false),
+ true <- visible_for_user?(quote, opts[:for]),
+ false <- User.blocks?(opts[:for], quoted_user),
+ false <- User.mutes?(opts[:for], quoted_user) do
opts =
opts
|> Map.put(:activity, quote)
|> Map.put(:do_not_recurse, true)
render("show.json", opts)
+ else
+ _ -> nil
end
end
end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
index a6f8f3fc8..f46dded7c 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -428,6 +428,34 @@ test "a quote that we can't resolve" do
assert is_nil(status.quote)
end
+ test "a quote from a user we block" do
+ user = insert(:user)
+ other_user = insert(:user)
+ blocked_user = insert(:user)
+
+ {:ok, _relationship} = User.block(user, blocked_user)
+
+ {:ok, activity} = CommonAPI.post(blocked_user, %{status: ":< i am ANGERY"})
+ {:ok, quote_activity} = CommonAPI.post(other_user, %{status: "hehe", quote_id: activity.id})
+
+ status = StatusView.render("show.json", %{activity: quote_activity, for: user})
+ assert is_nil(status.quote)
+ end
+
+ test "a quote from a user we mute" do
+ user = insert(:user)
+ other_user = insert(:user)
+ blocked_user = insert(:user)
+
+ {:ok, _relationship} = User.mute(user, blocked_user)
+
+ {:ok, activity} = CommonAPI.post(blocked_user, %{status: ":< i am ANGERY"})
+ {:ok, quote_activity} = CommonAPI.post(other_user, %{status: "hehe", quote_id: activity.id})
+
+ status = StatusView.render("show.json", %{activity: quote_activity, for: user})
+ assert is_nil(status.quote)
+ end
+
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
From b0130bfa7b420550aa7acba6a88c71aa22c51246 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Sun, 21 Aug 2022 16:22:15 +0100
Subject: [PATCH 12/44] Revert "oauth2 fixes (#177)"
This reverts commit 429e2ac832a874ae8ba8a9c116da61a6273c8a87.
---
lib/pleroma/helpers/auth_helper.ex | 18 +++++
.../controllers/auth_controller.ex | 11 ++-
lib/pleroma/web/o_auth/o_auth_controller.ex | 77 +++++++++----------
lib/pleroma/web/o_auth/token.ex | 6 --
lib/pleroma/web/o_auth/token/query.ex | 8 +-
lib/pleroma/web/plugs/o_auth_plug.ex | 13 +++-
.../web/plugs/set_user_session_id_plug.ex | 18 +++++
lib/pleroma/web/router.ex | 5 +-
mix.exs | 4 +-
.../web/o_auth/o_auth_controller_test.exs | 53 +++++++++++--
test/pleroma/web/plugs/o_auth_plug_test.exs | 56 ++++++++++++++
.../plugs/set_user_session_id_plug_test.exs | 43 +++++++++++
12 files changed, 243 insertions(+), 69 deletions(-)
create mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex
create mode 100644 test/pleroma/web/plugs/set_user_session_id_plug_test.exs
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index d56f6f461..13e4c8158 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -4,9 +4,12 @@
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Plug.Conn
import Plug.Conn
+ @oauth_token_session_key :oauth_token
+
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
@@ -25,4 +28,19 @@ def drop_auth_info(conn) do
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
+
+ @doc "Gets OAuth token string from session"
+ def get_session_token(%Conn{} = conn) do
+ get_session(conn, @oauth_token_session_key)
+ end
+
+ @doc "Updates OAuth token string in session"
+ def put_session_token(%Conn{} = conn, token) when is_binary(token) do
+ put_session(conn, @oauth_token_session_key, token)
+ end
+
+ @doc "Deletes OAuth token string from session"
+ def delete_session_token(%Conn{} = conn) do
+ delete_session(conn, @oauth_token_session_key)
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index f415e5931..4920d65da 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -33,6 +34,7 @@ def login(conn, %{"code" => auth_token} = params) do
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
+ |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
@@ -40,9 +42,9 @@ def login(conn, %{"code" => auth_token} = params) do
end
def login(conn, params) do
- with %{assigns: %{user: %User{}, token: %Token{app_id: app_id, token: token}}} <- conn,
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
- redirect(conn, to: local_mastodon_post_login_path(conn) <> "?access_token=#{token}")
+ redirect(conn, to: local_mastodon_post_login_path(conn))
else
_ -> redirect_to_oauth_form(conn, params)
end
@@ -66,8 +68,9 @@ defp redirect_to_oauth_form(conn, _params) do
def logout(conn, _) do
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
- {:ok, %Token{token: _session_token}} <- RevokeToken.revoke(oauth_token) do
- conn
+ session_token = AuthHelper.get_session_token(conn),
+ {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
+ AuthHelper.delete_session_token(conn)
else
_ -> conn
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 43536f95d..358120fe6 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@@ -76,46 +77,33 @@ defp do_authorize(%Plug.Conn{} = conn, params) do
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
- # if we already have a token for this specific setup, we can use that
- with false <- Params.truthy_param?(params["force_login"]),
- %App{} <- app,
- {:ok, _} <- Scopes.validate(scopes, app.scopes),
- {:ok, %Token{} = token} <- Token.get_by_app(app) do
- token = Repo.preload(token, :app)
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
- conn
- |> assign(:token, token)
- |> handle_existing_authorization(params)
- else
- _ ->
- user =
- with %{assigns: %{user: %User{} = user}} <- conn do
- user
- else
- _ -> nil
- end
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
- scopes =
- if scopes == [] do
- available_scopes
- else
- scopes
- end
-
- # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
- render(conn, Authenticator.auth_template(), %{
- user: user,
- app: app && Map.delete(app, :client_secret),
- response_type: params["response_type"],
- client_id: params["client_id"],
- available_scopes: available_scopes,
- scopes: scopes,
- redirect_uri: params["redirect_uri"],
- state: params["state"],
- params: params,
- view_module: OAuthView
- })
- end
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+ render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
+ response_type: params["response_type"],
+ client_id: params["client_id"],
+ available_scopes: available_scopes,
+ scopes: scopes,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params,
+ view_module: OAuthView
+ })
end
defp handle_existing_authorization(
@@ -330,8 +318,9 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
- def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
+ |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
@@ -390,7 +379,15 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
- {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
+ {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
+ conn =
+ with session_token = AuthHelper.get_session_token(conn),
+ %Token{token: ^session_token} <- oauth_token do
+ AuthHelper.delete_session_token(conn)
+ else
+ _ -> conn
+ end
+
json(conn, %{})
else
_error ->
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 08c8cd298..9d69e9db4 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -39,12 +39,6 @@ def get_by_token(token) do
|> Repo.find_resource()
end
- def get_by_app(%App{} = app) do
- app.id
- |> Query.get_unexpired_by_app()
- |> Repo.find_resource()
- end
-
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index 8edfcf5d7..d16a759d8 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -23,15 +23,9 @@ def get_by_token(query \\ Token, token) do
from(q in query, where: q.token == ^token)
end
- @spec get_unexpired_by_app(query, String.t()) :: query
- def get_unexpired_by_app(query \\ Token, app_id) do
- time = NaiveDateTime.utc_now()
- from(q in query, where: q.app_id == ^app_id and q.valid_until > ^time, limit: 1)
- end
-
@spec get_by_app(query, String.t()) :: query
def get_by_app(query \\ Token, app_id) do
- from(q in query, where: q.app_id == ^app_id, limit: 1)
+ from(q in query, where: q.app_id == ^app_id)
end
@spec get_by_id(query, String.t()) :: query
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 29b3316b3..5e06ac3f6 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -17,6 +18,8 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def init(options), do: options
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
@@ -79,7 +82,7 @@ defp fetch_token_str(%Plug.Conn{} = conn) do
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
- _ -> :no_token_found
+ _ -> fetch_token_from_session(conn)
end
end
@@ -93,4 +96,12 @@ defp fetch_token_str([token | tail]) do
end
defp fetch_token_str([]), do: :no_token_found
+
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case AuthHelper.get_session_token(conn) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
new file mode 100644
index 000000000..a1cfa0915
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.OAuth.Token
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
+ AuthHelper.put_session_token(conn, oauth_token.token)
+ end
+
+ def call(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index f2d6b0aff..647d99278 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -57,6 +57,7 @@ defmodule Pleroma.Web.Router do
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
@@ -792,9 +793,11 @@ defmodule Pleroma.Web.Router do
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
- get("/web/*path", MastoFEController, :index)
+
post("/auth/password", MastodonAPI.AuthController, :password_reset)
+ get("/web/*path", MastoFEController, :index)
+
get("/embed/:id", EmbedController, :show)
end
diff --git a/mix.exs b/mix.exs
index 170276b0a..e7f491997 100644
--- a/mix.exs
+++ b/mix.exs
@@ -131,7 +131,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.0", override: true},
+ {:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
{:poison, "~> 3.0", override: true},
@@ -152,7 +152,7 @@ defp deps do
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
- {:swoosh, "~> 1.3"},
+ {:swoosh, "~> 1.0"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 4e197a485..0fdd5b8e9 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
@@ -455,7 +456,7 @@ test "renders authentication page if user is already authenticated but `force_lo
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -479,7 +480,7 @@ test "renders authentication page if user is already authenticated but user requ
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -502,7 +503,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -528,7 +529,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -552,7 +553,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -610,6 +611,41 @@ test "redirects with oauth authorization, " <>
end
end
+ test "authorize from cookie" do
+ user = insert(:user)
+ app = insert(:oauth_app)
+ oauth_token = insert(:oauth_token, user: user, app: app)
+ redirect_uri = OAuthController.default_redirect_uri(app)
+
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+ |> post(
+ "/oauth/authorize",
+ %{
+ "authorization" => %{
+ "name" => user.nickname,
+ "client_id" => app.client_id,
+ "redirect_uri" => redirect_uri,
+ "scope" => app.scopes,
+ "state" => "statepassed"
+ }
+ }
+ )
+
+ target = redirected_to(conn)
+ assert target =~ redirect_uri
+
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+
+ assert %{"state" => "statepassed", "code" => code} = query
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth
+ assert auth.scopes == app.scopes
+ end
+
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
@@ -1182,7 +1218,6 @@ test "issues a new token if token expired" do
response =
build_conn()
- |> put_req_header("authorization", "Bearer #{access_token.token}")
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
@@ -1232,11 +1267,12 @@ test "when authenticated with request token, revokes it and clears it from sessi
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
+ refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
@@ -1250,11 +1286,12 @@ test "if request is authenticated with a different token, " <>
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
+ assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs
index caabfc1cb..9e4be5559 100644
--- a/test/pleroma/web/plugs/o_auth_plug_test.exs
+++ b/test/pleroma/web/plugs/o_auth_plug_test.exs
@@ -5,8 +5,11 @@
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.Token.Strategy.Revoke
alias Pleroma.Web.Plugs.OAuthPlug
+ alias Plug.Session
import Pleroma.Factory
@@ -69,4 +72,57 @@ test "with invalid token, it does not assign the user", %{conn: conn} do
refute conn.assigns[:user]
end
+
+ describe "with :oauth_token in session, " do
+ setup %{token: oauth_token, conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Session.call(Session.init(session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+
+ %{conn: conn}
+ end
+
+ test "if session-stored token matches a valid OAuth token, assigns :user and :token", %{
+ conn: conn,
+ user: user,
+ token: oauth_token
+ } do
+ conn = OAuthPlug.call(conn, %{})
+
+ assert conn.assigns.user && conn.assigns.user.id == user.id
+ assert conn.assigns.token && conn.assigns.token.id == oauth_token.id
+ end
+
+ test "if session-stored token matches an expired OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ expired_valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600 * 24, :second)
+
+ oauth_token
+ |> Ecto.Changeset.change(valid_until: expired_valid_until)
+ |> Pleroma.Repo.update()
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+
+ test "if session-stored token matches a revoked OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ Revoke.revoke(oauth_token)
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+ end
end
diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
new file mode 100644
index 000000000..9814c80d8
--- /dev/null
+++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.Plugs.SetUserSessionIdPlug
+
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ %{conn: conn}
+ end
+
+ test "doesn't do anything if the user isn't set", %{conn: conn} do
+ ret_conn = SetUserSessionIdPlug.call(conn, %{})
+
+ assert ret_conn == conn
+ end
+
+ test "sets session token basing on :token assign", %{conn: conn} do
+ %{user: user, token: oauth_token} = oauth_access(["read"])
+
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, oauth_token)
+ |> SetUserSessionIdPlug.call(%{})
+
+ assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
+ end
+end
From aa681d7e15f6170e7e92d86146d5ba96be6433bc Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Sun, 21 Aug 2022 16:24:37 +0000
Subject: [PATCH 13/44] Fix oauth2 (for real) (#179)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/179
---
lib/pleroma/helpers/auth_helper.ex | 18 --
.../controllers/auth_controller.ex | 11 +-
lib/pleroma/web/o_auth/o_auth_controller.ex | 88 +++++----
lib/pleroma/web/o_auth/token.ex | 6 +
lib/pleroma/web/o_auth/token/query.ex | 12 +-
lib/pleroma/web/plugs/o_auth_plug.ex | 13 +-
.../web/plugs/set_user_session_id_plug.ex | 18 --
lib/pleroma/web/router.ex | 5 +-
mix.exs | 4 +-
.../web/o_auth/o_auth_controller_test.exs | 167 ++++++++++++------
test/pleroma/web/plugs/o_auth_plug_test.exs | 56 ------
.../plugs/set_user_session_id_plug_test.exs | 43 -----
12 files changed, 193 insertions(+), 248 deletions(-)
delete mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex
delete mode 100644 test/pleroma/web/plugs/set_user_session_id_plug_test.exs
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index 13e4c8158..d56f6f461 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -4,12 +4,9 @@
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
- alias Plug.Conn
import Plug.Conn
- @oauth_token_session_key :oauth_token
-
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
@@ -28,19 +25,4 @@ def drop_auth_info(conn) do
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
-
- @doc "Gets OAuth token string from session"
- def get_session_token(%Conn{} = conn) do
- get_session(conn, @oauth_token_session_key)
- end
-
- @doc "Updates OAuth token string in session"
- def put_session_token(%Conn{} = conn, token) when is_binary(token) do
- put_session(conn, @oauth_token_session_key, token)
- end
-
- @doc "Deletes OAuth token string from session"
- def delete_session_token(%Conn{} = conn) do
- delete_session(conn, @oauth_token_session_key)
- end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 4920d65da..f415e5931 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -34,7 +33,6 @@ def login(conn, %{"code" => auth_token} = params) do
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
- |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
@@ -42,9 +40,9 @@ def login(conn, %{"code" => auth_token} = params) do
end
def login(conn, params) do
- with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id, token: token}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
- redirect(conn, to: local_mastodon_post_login_path(conn))
+ redirect(conn, to: local_mastodon_post_login_path(conn) <> "?access_token=#{token}")
else
_ -> redirect_to_oauth_form(conn, params)
end
@@ -68,9 +66,8 @@ defp redirect_to_oauth_form(conn, _params) do
def logout(conn, _) do
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
- session_token = AuthHelper.get_session_token(conn),
- {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
- AuthHelper.delete_session_token(conn)
+ {:ok, %Token{token: _session_token}} <- RevokeToken.revoke(oauth_token) do
+ conn
else
_ -> conn
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 358120fe6..6a8006d31 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@@ -72,38 +71,62 @@ def authorize(
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
+ defp maybe_remove_token(%Plug.Conn{assigns: %{token: %{app: id}}} = conn, %App{id: id}) do
+ conn
+ end
+
+ defp maybe_remove_token(conn, _app) do
+ conn
+ |> assign(:token, nil)
+ end
+
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
+ conn = maybe_remove_token(conn, app)
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
- user =
- with %{assigns: %{user: %User{} = user}} <- conn do
- user
- else
- _ -> nil
- end
+ # if we already have a token for this specific setup, we can use that
+ with false <- Params.truthy_param?(params["force_login"]),
+ %App{} <- app,
+ %{assigns: %{user: %Pleroma.User{} = user}} <- conn,
+ {:ok, %Token{} = token} <- Token.get_preexisting_by_app_and_user(app, user),
+ true <- scopes == token.scopes do
+ token = Repo.preload(token, :app)
- scopes =
- if scopes == [] do
- available_scopes
- else
- scopes
- end
+ conn
+ |> assign(:token, token)
+ |> handle_existing_authorization(params)
+ else
+ _ ->
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
- # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
- render(conn, Authenticator.auth_template(), %{
- user: user,
- app: app && Map.delete(app, :client_secret),
- response_type: params["response_type"],
- client_id: params["client_id"],
- available_scopes: available_scopes,
- scopes: scopes,
- redirect_uri: params["redirect_uri"],
- state: params["state"],
- params: params,
- view_module: OAuthView
- })
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
+
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+ render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
+ response_type: params["response_type"],
+ client_id: params["client_id"],
+ available_scopes: available_scopes,
+ scopes: scopes,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params,
+ view_module: OAuthView
+ })
+ end
end
defp handle_existing_authorization(
@@ -318,9 +341,8 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
- def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
conn
- |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
@@ -379,15 +401,7 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
- {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
- conn =
- with session_token = AuthHelper.get_session_token(conn),
- %Token{token: ^session_token} <- oauth_token do
- AuthHelper.delete_session_token(conn)
- else
- _ -> conn
- end
-
+ {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
json(conn, %{})
else
_error ->
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 9d69e9db4..6e91b6216 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -39,6 +39,12 @@ def get_by_token(token) do
|> Repo.find_resource()
end
+ def get_preexisting_by_app_and_user(%App{} = app, %User{} = user) do
+ app.id
+ |> Query.get_unexpired_by_app_and_user(user)
+ |> Repo.find_resource()
+ end
+
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index d16a759d8..acddf0533 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -23,9 +23,19 @@ def get_by_token(query \\ Token, token) do
from(q in query, where: q.token == ^token)
end
+ @spec get_unexpired_by_app_and_user(query, String.t()) :: query
+ def get_unexpired_by_app_and_user(query \\ Token, app_id, %Pleroma.User{id: user_id}) do
+ time = NaiveDateTime.utc_now()
+
+ from(q in query,
+ where: q.app_id == ^app_id and q.valid_until > ^time and q.user_id == ^user_id,
+ limit: 1
+ )
+ end
+
@spec get_by_app(query, String.t()) :: query
def get_by_app(query \\ Token, app_id) do
- from(q in query, where: q.app_id == ^app_id)
+ from(q in query, where: q.app_id == ^app_id, limit: 1)
end
@spec get_by_id(query, String.t()) :: query
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 5e06ac3f6..29b3316b3 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -18,8 +17,6 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def init(options), do: options
- def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
-
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
@@ -82,7 +79,7 @@ defp fetch_token_str(%Plug.Conn{} = conn) do
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
- _ -> fetch_token_from_session(conn)
+ _ -> :no_token_found
end
end
@@ -96,12 +93,4 @@ defp fetch_token_str([token | tail]) do
end
defp fetch_token_str([]), do: :no_token_found
-
- @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
- defp fetch_token_from_session(conn) do
- case AuthHelper.get_session_token(conn) do
- nil -> :no_token_found
- token -> {:ok, token}
- end
- end
end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
deleted file mode 100644
index a1cfa0915..000000000
--- a/lib/pleroma/web/plugs/set_user_session_id_plug.ex
+++ /dev/null
@@ -1,18 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
- alias Pleroma.Helpers.AuthHelper
- alias Pleroma.Web.OAuth.Token
-
- def init(opts) do
- opts
- end
-
- def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
- AuthHelper.put_session_token(conn, oauth_token.token)
- end
-
- def call(conn, _), do: conn
-end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 647d99278..f2d6b0aff 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -57,7 +57,6 @@ defmodule Pleroma.Web.Router do
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
- plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
@@ -793,10 +792,8 @@ defmodule Pleroma.Web.Router do
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
-
- post("/auth/password", MastodonAPI.AuthController, :password_reset)
-
get("/web/*path", MastoFEController, :index)
+ post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/embed/:id", EmbedController, :show)
end
diff --git a/mix.exs b/mix.exs
index e7f491997..170276b0a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -131,7 +131,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.1", override: true},
+ {:phoenix_html, "~> 3.0", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
{:poison, "~> 3.0", override: true},
@@ -152,7 +152,7 @@ defp deps do
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
- {:swoosh, "~> 1.0"},
+ {:swoosh, "~> 1.3"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 0fdd5b8e9..9f984b26f 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
@@ -456,7 +455,7 @@ test "renders authentication page if user is already authenticated but `force_lo
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -471,26 +470,130 @@ test "renders authentication page if user is already authenticated but `force_lo
assert html_response(conn, 200) =~ ~s(type="submit")
end
- test "renders authentication page if user is already authenticated but user request with another client",
+ test "reuses authentication if the user is authenticated with another client",
%{
- app: app,
conn: conn
} do
- token = insert(:oauth_token, app: app)
+ user = insert(:user)
+
+ app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+ other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ token = insert(:oauth_token, user: user, app: app)
+ reusable_token = insert(:oauth_token, app: other_app, user: user)
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
- "client_id" => "another_client_id",
- "redirect_uri" => OAuthController.default_redirect_uri(app),
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
"scope" => "read"
}
)
+ assert URI.decode(redirected_to(conn)) ==
+ "https://redirect.url?access_token=#{reusable_token.token}"
+ end
+
+ test "does not reuse other people's tokens",
+ %{
+ conn: conn
+ } do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+ other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ token = insert(:oauth_token, user: user, app: app)
+ _not_reusable_token = insert(:oauth_token, app: other_app, user: other_user)
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
+ test "does not reuse expired tokens",
+ %{
+ conn: conn
+ } do
+ user = insert(:user)
+
+ app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ token = insert(:oauth_token, user: user, app: app)
+
+ _not_reusable_token =
+ insert(:oauth_token,
+ app: other_app,
+ user: user,
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -60 * 100)
+ )
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
+ test "does not reuse tokens with the wrong scopes",
+ %{
+ conn: conn
+ } do
+ user = insert(:user)
+
+ app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
+
+ token = insert(:oauth_token, user: user, app: app, scopes: ["read"])
+
+ _not_reusable_token =
+ insert(:oauth_token,
+ app: other_app,
+ user: user
+ )
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer #{token.token}")
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read write"
+ }
+ )
+
assert html_response(conn, 200) =~ ~s(type="submit")
end
@@ -503,7 +606,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -529,7 +632,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -553,7 +656,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
conn =
conn
- |> AuthHelper.put_session_token(token.token)
+ |> put_req_header("authorization", "Bearer #{token.token}")
|> get(
"/oauth/authorize",
%{
@@ -611,41 +714,6 @@ test "redirects with oauth authorization, " <>
end
end
- test "authorize from cookie" do
- user = insert(:user)
- app = insert(:oauth_app)
- oauth_token = insert(:oauth_token, user: user, app: app)
- redirect_uri = OAuthController.default_redirect_uri(app)
-
- conn =
- build_conn()
- |> Plug.Session.call(Plug.Session.init(@session_opts))
- |> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
- |> post(
- "/oauth/authorize",
- %{
- "authorization" => %{
- "name" => user.nickname,
- "client_id" => app.client_id,
- "redirect_uri" => redirect_uri,
- "scope" => app.scopes,
- "state" => "statepassed"
- }
- }
- )
-
- target = redirected_to(conn)
- assert target =~ redirect_uri
-
- query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
-
- assert %{"state" => "statepassed", "code" => code} = query
- auth = Repo.get_by(Authorization, token: code)
- assert auth
- assert auth.scopes == app.scopes
- end
-
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
@@ -1218,6 +1286,7 @@ test "issues a new token if token expired" do
response =
build_conn()
+ |> put_req_header("authorization", "Bearer #{access_token.token}")
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
@@ -1267,12 +1336,11 @@ test "when authenticated with request token, revokes it and clears it from sessi
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
+ |> put_req_header("authorization", "Bearer #{oauth_token.token}")
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
- refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
@@ -1286,12 +1354,11 @@ test "if request is authenticated with a different token, " <>
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
+ |> put_req_header("authorization", "Bearer #{oauth_token.token}")
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
- assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs
index 9e4be5559..caabfc1cb 100644
--- a/test/pleroma/web/plugs/o_auth_plug_test.exs
+++ b/test/pleroma/web/plugs/o_auth_plug_test.exs
@@ -5,11 +5,8 @@
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
- alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.OAuth.Token.Strategy.Revoke
alias Pleroma.Web.Plugs.OAuthPlug
- alias Plug.Session
import Pleroma.Factory
@@ -72,57 +69,4 @@ test "with invalid token, it does not assign the user", %{conn: conn} do
refute conn.assigns[:user]
end
-
- describe "with :oauth_token in session, " do
- setup %{token: oauth_token, conn: conn} do
- session_opts = [
- store: :cookie,
- key: "_test",
- signing_salt: "cooldude"
- ]
-
- conn =
- conn
- |> Session.call(Session.init(session_opts))
- |> fetch_session()
- |> AuthHelper.put_session_token(oauth_token.token)
-
- %{conn: conn}
- end
-
- test "if session-stored token matches a valid OAuth token, assigns :user and :token", %{
- conn: conn,
- user: user,
- token: oauth_token
- } do
- conn = OAuthPlug.call(conn, %{})
-
- assert conn.assigns.user && conn.assigns.user.id == user.id
- assert conn.assigns.token && conn.assigns.token.id == oauth_token.id
- end
-
- test "if session-stored token matches an expired OAuth token, does nothing", %{
- conn: conn,
- token: oauth_token
- } do
- expired_valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600 * 24, :second)
-
- oauth_token
- |> Ecto.Changeset.change(valid_until: expired_valid_until)
- |> Pleroma.Repo.update()
-
- ret_conn = OAuthPlug.call(conn, %{})
- assert ret_conn == conn
- end
-
- test "if session-stored token matches a revoked OAuth token, does nothing", %{
- conn: conn,
- token: oauth_token
- } do
- Revoke.revoke(oauth_token)
-
- ret_conn = OAuthPlug.call(conn, %{})
- assert ret_conn == conn
- end
- end
end
diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
deleted file mode 100644
index 9814c80d8..000000000
--- a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
+++ /dev/null
@@ -1,43 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
- use Pleroma.Web.ConnCase, async: true
-
- alias Pleroma.Helpers.AuthHelper
- alias Pleroma.Web.Plugs.SetUserSessionIdPlug
-
- setup %{conn: conn} do
- session_opts = [
- store: :cookie,
- key: "_test",
- signing_salt: "cooldude"
- ]
-
- conn =
- conn
- |> Plug.Session.call(Plug.Session.init(session_opts))
- |> fetch_session()
-
- %{conn: conn}
- end
-
- test "doesn't do anything if the user isn't set", %{conn: conn} do
- ret_conn = SetUserSessionIdPlug.call(conn, %{})
-
- assert ret_conn == conn
- end
-
- test "sets session token basing on :token assign", %{conn: conn} do
- %{user: user, token: oauth_token} = oauth_access(["read"])
-
- ret_conn =
- conn
- |> assign(:user, user)
- |> assign(:token, oauth_token)
- |> SetUserSessionIdPlug.call(%{})
-
- assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
- end
-end
From 8d7b63a766e8e2e254843a28ff417d3da57a12be Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Sun, 21 Aug 2022 17:52:02 +0100
Subject: [PATCH 14/44] Revert "Fix oauth2 (for real) (#179)"
This reverts commit aa681d7e15f6170e7e92d86146d5ba96be6433bc.
---
lib/pleroma/helpers/auth_helper.ex | 18 ++
.../controllers/auth_controller.ex | 11 +-
lib/pleroma/web/o_auth/o_auth_controller.ex | 88 ++++-----
lib/pleroma/web/o_auth/token.ex | 6 -
lib/pleroma/web/o_auth/token/query.ex | 12 +-
lib/pleroma/web/plugs/o_auth_plug.ex | 13 +-
.../web/plugs/set_user_session_id_plug.ex | 18 ++
lib/pleroma/web/router.ex | 5 +-
mix.exs | 4 +-
.../web/o_auth/o_auth_controller_test.exs | 167 ++++++------------
test/pleroma/web/plugs/o_auth_plug_test.exs | 56 ++++++
.../plugs/set_user_session_id_plug_test.exs | 43 +++++
12 files changed, 248 insertions(+), 193 deletions(-)
create mode 100644 lib/pleroma/web/plugs/set_user_session_id_plug.ex
create mode 100644 test/pleroma/web/plugs/set_user_session_id_plug_test.exs
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index d56f6f461..13e4c8158 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -4,9 +4,12 @@
defmodule Pleroma.Helpers.AuthHelper do
alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Plug.Conn
import Plug.Conn
+ @oauth_token_session_key :oauth_token
+
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
Intended to be used with explicit authentication and only when OAuth token cannot be determined.
@@ -25,4 +28,19 @@ def drop_auth_info(conn) do
|> assign(:token, nil)
|> put_private(:authentication_ignored, true)
end
+
+ @doc "Gets OAuth token string from session"
+ def get_session_token(%Conn{} = conn) do
+ get_session(conn, @oauth_token_session_key)
+ end
+
+ @doc "Updates OAuth token string in session"
+ def put_session_token(%Conn{} = conn, token) when is_binary(token) do
+ put_session(conn, @oauth_token_session_key, token)
+ end
+
+ @doc "Deletes OAuth token string from session"
+ def delete_session_token(%Conn{} = conn) do
+ delete_session(conn, @oauth_token_session_key)
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index f415e5931..4920d65da 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -33,6 +34,7 @@ def login(conn, %{"code" => auth_token} = params) do
|> UriHelper.modify_uri_params(%{"access_token" => oauth_token.token})
conn
+ |> AuthHelper.put_session_token(oauth_token.token)
|> redirect(to: redirect_to)
else
_ -> redirect_to_oauth_form(conn, params)
@@ -40,9 +42,9 @@ def login(conn, %{"code" => auth_token} = params) do
end
def login(conn, params) do
- with %{assigns: %{user: %User{}, token: %Token{app_id: app_id, token: token}}} <- conn,
+ with %{assigns: %{user: %User{}, token: %Token{app_id: app_id}}} <- conn,
{:ok, %{id: ^app_id}} <- local_mastofe_app() do
- redirect(conn, to: local_mastodon_post_login_path(conn) <> "?access_token=#{token}")
+ redirect(conn, to: local_mastodon_post_login_path(conn))
else
_ -> redirect_to_oauth_form(conn, params)
end
@@ -66,8 +68,9 @@ defp redirect_to_oauth_form(conn, _params) do
def logout(conn, _) do
conn =
with %{assigns: %{token: %Token{} = oauth_token}} <- conn,
- {:ok, %Token{token: _session_token}} <- RevokeToken.revoke(oauth_token) do
- conn
+ session_token = AuthHelper.get_session_token(conn),
+ {:ok, %Token{token: ^session_token}} <- RevokeToken.revoke(oauth_token) do
+ AuthHelper.delete_session_token(conn)
else
_ -> conn
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 6a8006d31..358120fe6 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Helpers.UriHelper
alias Pleroma.Maps
alias Pleroma.MFA
@@ -71,62 +72,38 @@ def authorize(
def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
- defp maybe_remove_token(%Plug.Conn{assigns: %{token: %{app: id}}} = conn, %App{id: id}) do
- conn
- end
-
- defp maybe_remove_token(conn, _app) do
- conn
- |> assign(:token, nil)
- end
-
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
- conn = maybe_remove_token(conn, app)
available_scopes = (app && app.scopes) || []
scopes = Scopes.fetch_scopes(params, available_scopes)
- # if we already have a token for this specific setup, we can use that
- with false <- Params.truthy_param?(params["force_login"]),
- %App{} <- app,
- %{assigns: %{user: %Pleroma.User{} = user}} <- conn,
- {:ok, %Token{} = token} <- Token.get_preexisting_by_app_and_user(app, user),
- true <- scopes == token.scopes do
- token = Repo.preload(token, :app)
+ user =
+ with %{assigns: %{user: %User{} = user}} <- conn do
+ user
+ else
+ _ -> nil
+ end
- conn
- |> assign(:token, token)
- |> handle_existing_authorization(params)
- else
- _ ->
- user =
- with %{assigns: %{user: %User{} = user}} <- conn do
- user
- else
- _ -> nil
- end
+ scopes =
+ if scopes == [] do
+ available_scopes
+ else
+ scopes
+ end
- scopes =
- if scopes == [] do
- available_scopes
- else
- scopes
- end
-
- # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
- render(conn, Authenticator.auth_template(), %{
- user: user,
- app: app && Map.delete(app, :client_secret),
- response_type: params["response_type"],
- client_id: params["client_id"],
- available_scopes: available_scopes,
- scopes: scopes,
- redirect_uri: params["redirect_uri"],
- state: params["state"],
- params: params,
- view_module: OAuthView
- })
- end
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
+ render(conn, Authenticator.auth_template(), %{
+ user: user,
+ app: app && Map.delete(app, :client_secret),
+ response_type: params["response_type"],
+ client_id: params["client_id"],
+ available_scopes: available_scopes,
+ scopes: scopes,
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params,
+ view_module: OAuthView
+ })
end
defp handle_existing_authorization(
@@ -341,8 +318,9 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"}
# Bad request
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
- def after_token_exchange(%Plug.Conn{} = conn, %{token: _token} = view_params) do
+ def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
+ |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params))
end
@@ -401,7 +379,15 @@ defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
def token_revoke(%Plug.Conn{} = conn, %{"token" => token}) do
with {:ok, %Token{} = oauth_token} <- Token.get_by_token(token),
- {:ok, _oauth_token} <- RevokeToken.revoke(oauth_token) do
+ {:ok, oauth_token} <- RevokeToken.revoke(oauth_token) do
+ conn =
+ with session_token = AuthHelper.get_session_token(conn),
+ %Token{token: ^session_token} <- oauth_token do
+ AuthHelper.delete_session_token(conn)
+ else
+ _ -> conn
+ end
+
json(conn, %{})
else
_error ->
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 6e91b6216..9d69e9db4 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -39,12 +39,6 @@ def get_by_token(token) do
|> Repo.find_resource()
end
- def get_preexisting_by_app_and_user(%App{} = app, %User{} = user) do
- app.id
- |> Query.get_unexpired_by_app_and_user(user)
- |> Repo.find_resource()
- end
-
@doc "Gets token for app by access token"
@spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
def get_by_token(%App{id: app_id} = _app, token) do
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index acddf0533..d16a759d8 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -23,19 +23,9 @@ def get_by_token(query \\ Token, token) do
from(q in query, where: q.token == ^token)
end
- @spec get_unexpired_by_app_and_user(query, String.t()) :: query
- def get_unexpired_by_app_and_user(query \\ Token, app_id, %Pleroma.User{id: user_id}) do
- time = NaiveDateTime.utc_now()
-
- from(q in query,
- where: q.app_id == ^app_id and q.valid_until > ^time and q.user_id == ^user_id,
- limit: 1
- )
- end
-
@spec get_by_app(query, String.t()) :: query
def get_by_app(query \\ Token, app_id) do
- from(q in query, where: q.app_id == ^app_id, limit: 1)
+ from(q in query, where: q.app_id == ^app_id)
end
@spec get_by_id(query, String.t()) :: query
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
index 29b3316b3..5e06ac3f6 100644
--- a/lib/pleroma/web/plugs/o_auth_plug.ex
+++ b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
import Plug.Conn
import Ecto.Query
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.App
@@ -17,6 +18,8 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def init(options), do: options
+ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
+
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
@@ -79,7 +82,7 @@ defp fetch_token_str(%Plug.Conn{} = conn) do
with {:ok, token} <- fetch_token_str(headers) do
{:ok, token}
else
- _ -> :no_token_found
+ _ -> fetch_token_from_session(conn)
end
end
@@ -93,4 +96,12 @@ defp fetch_token_str([token | tail]) do
end
defp fetch_token_str([]), do: :no_token_found
+
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case AuthHelper.get_session_token(conn) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
end
diff --git a/lib/pleroma/web/plugs/set_user_session_id_plug.ex b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
new file mode 100644
index 000000000..a1cfa0915
--- /dev/null
+++ b/lib/pleroma/web/plugs/set_user_session_id_plug.ex
@@ -0,0 +1,18 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlug do
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.OAuth.Token
+
+ def init(opts) do
+ opts
+ end
+
+ def call(%{assigns: %{token: %Token{} = oauth_token}} = conn, _) do
+ AuthHelper.put_session_token(conn, oauth_token.token)
+ end
+
+ def call(conn, _), do: conn
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index f2d6b0aff..647d99278 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -57,6 +57,7 @@ defmodule Pleroma.Web.Router do
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
+ plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
@@ -792,9 +793,11 @@ defmodule Pleroma.Web.Router do
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
- get("/web/*path", MastoFEController, :index)
+
post("/auth/password", MastodonAPI.AuthController, :password_reset)
+ get("/web/*path", MastoFEController, :index)
+
get("/embed/:id", EmbedController, :show)
end
diff --git a/mix.exs b/mix.exs
index 170276b0a..e7f491997 100644
--- a/mix.exs
+++ b/mix.exs
@@ -131,7 +131,7 @@ defp deps do
{:trailing_format_plug, "~> 0.0.7"},
{:fast_sanitize, "~> 0.2.3"},
{:html_entities, "~> 0.5", override: true},
- {:phoenix_html, "~> 3.0", override: true},
+ {:phoenix_html, "~> 3.1", override: true},
{:calendar, "~> 1.0"},
{:cachex, "~> 3.4"},
{:poison, "~> 3.0", override: true},
@@ -152,7 +152,7 @@ defp deps do
ref: "f75cd55325e33cbea198fb41fe41871392f8fb76"},
{:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"},
- {:swoosh, "~> 1.3"},
+ {:swoosh, "~> 1.0"},
{:phoenix_swoosh, "~> 0.3"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 9f984b26f..0fdd5b8e9 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
import Pleroma.Factory
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
@@ -455,7 +456,7 @@ test "renders authentication page if user is already authenticated but `force_lo
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -470,130 +471,26 @@ test "renders authentication page if user is already authenticated but `force_lo
assert html_response(conn, 200) =~ ~s(type="submit")
end
- test "reuses authentication if the user is authenticated with another client",
+ test "renders authentication page if user is already authenticated but user request with another client",
%{
+ app: app,
conn: conn
} do
- user = insert(:user)
-
- app = insert(:oauth_app, redirect_uris: "https://redirect.url")
- other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- token = insert(:oauth_token, user: user, app: app)
- reusable_token = insert(:oauth_token, app: other_app, user: user)
+ token = insert(:oauth_token, app: app)
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
- "client_id" => other_app.client_id,
- "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "client_id" => "another_client_id",
+ "redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
- assert URI.decode(redirected_to(conn)) ==
- "https://redirect.url?access_token=#{reusable_token.token}"
- end
-
- test "does not reuse other people's tokens",
- %{
- conn: conn
- } do
- user = insert(:user)
- other_user = insert(:user)
-
- app = insert(:oauth_app, redirect_uris: "https://redirect.url")
- other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- token = insert(:oauth_token, user: user, app: app)
- _not_reusable_token = insert(:oauth_token, app: other_app, user: other_user)
-
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> get(
- "/oauth/authorize",
- %{
- "response_type" => "code",
- "client_id" => other_app.client_id,
- "redirect_uri" => OAuthController.default_redirect_uri(other_app),
- "scope" => "read"
- }
- )
-
- assert html_response(conn, 200) =~ ~s(type="submit")
- end
-
- test "does not reuse expired tokens",
- %{
- conn: conn
- } do
- user = insert(:user)
-
- app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- token = insert(:oauth_token, user: user, app: app)
-
- _not_reusable_token =
- insert(:oauth_token,
- app: other_app,
- user: user,
- valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -60 * 100)
- )
-
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> get(
- "/oauth/authorize",
- %{
- "response_type" => "code",
- "client_id" => other_app.client_id,
- "redirect_uri" => OAuthController.default_redirect_uri(other_app),
- "scope" => "read"
- }
- )
-
- assert html_response(conn, 200) =~ ~s(type="submit")
- end
-
- test "does not reuse tokens with the wrong scopes",
- %{
- conn: conn
- } do
- user = insert(:user)
-
- app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- other_app = insert(:oauth_app, redirect_uris: "https://redirect.url")
-
- token = insert(:oauth_token, user: user, app: app, scopes: ["read"])
-
- _not_reusable_token =
- insert(:oauth_token,
- app: other_app,
- user: user
- )
-
- conn =
- conn
- |> put_req_header("authorization", "Bearer #{token.token}")
- |> get(
- "/oauth/authorize",
- %{
- "response_type" => "code",
- "client_id" => other_app.client_id,
- "redirect_uri" => OAuthController.default_redirect_uri(other_app),
- "scope" => "read write"
- }
- )
-
assert html_response(conn, 200) =~ ~s(type="submit")
end
@@ -606,7 +503,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -632,7 +529,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -656,7 +553,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
conn =
conn
- |> put_req_header("authorization", "Bearer #{token.token}")
+ |> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
@@ -714,6 +611,41 @@ test "redirects with oauth authorization, " <>
end
end
+ test "authorize from cookie" do
+ user = insert(:user)
+ app = insert(:oauth_app)
+ oauth_token = insert(:oauth_token, user: user, app: app)
+ redirect_uri = OAuthController.default_redirect_uri(app)
+
+ conn =
+ build_conn()
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+ |> post(
+ "/oauth/authorize",
+ %{
+ "authorization" => %{
+ "name" => user.nickname,
+ "client_id" => app.client_id,
+ "redirect_uri" => redirect_uri,
+ "scope" => app.scopes,
+ "state" => "statepassed"
+ }
+ }
+ )
+
+ target = redirected_to(conn)
+ assert target =~ redirect_uri
+
+ query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
+
+ assert %{"state" => "statepassed", "code" => code} = query
+ auth = Repo.get_by(Authorization, token: code)
+ assert auth
+ assert auth.scopes == app.scopes
+ end
+
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
@@ -1286,7 +1218,6 @@ test "issues a new token if token expired" do
response =
build_conn()
- |> put_req_header("authorization", "Bearer #{access_token.token}")
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
@@ -1336,11 +1267,12 @@ test "when authenticated with request token, revokes it and clears it from sessi
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
+ refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
@@ -1354,11 +1286,12 @@ test "if request is authenticated with a different token, " <>
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
- |> put_req_header("authorization", "Bearer #{oauth_token.token}")
+ |> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
+ assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
diff --git a/test/pleroma/web/plugs/o_auth_plug_test.exs b/test/pleroma/web/plugs/o_auth_plug_test.exs
index caabfc1cb..9e4be5559 100644
--- a/test/pleroma/web/plugs/o_auth_plug_test.exs
+++ b/test/pleroma/web/plugs/o_auth_plug_test.exs
@@ -5,8 +5,11 @@
defmodule Pleroma.Web.Plugs.OAuthPlugTest do
use Pleroma.Web.ConnCase, async: true
+ alias Pleroma.Helpers.AuthHelper
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.OAuth.Token.Strategy.Revoke
alias Pleroma.Web.Plugs.OAuthPlug
+ alias Plug.Session
import Pleroma.Factory
@@ -69,4 +72,57 @@ test "with invalid token, it does not assign the user", %{conn: conn} do
refute conn.assigns[:user]
end
+
+ describe "with :oauth_token in session, " do
+ setup %{token: oauth_token, conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Session.call(Session.init(session_opts))
+ |> fetch_session()
+ |> AuthHelper.put_session_token(oauth_token.token)
+
+ %{conn: conn}
+ end
+
+ test "if session-stored token matches a valid OAuth token, assigns :user and :token", %{
+ conn: conn,
+ user: user,
+ token: oauth_token
+ } do
+ conn = OAuthPlug.call(conn, %{})
+
+ assert conn.assigns.user && conn.assigns.user.id == user.id
+ assert conn.assigns.token && conn.assigns.token.id == oauth_token.id
+ end
+
+ test "if session-stored token matches an expired OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ expired_valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600 * 24, :second)
+
+ oauth_token
+ |> Ecto.Changeset.change(valid_until: expired_valid_until)
+ |> Pleroma.Repo.update()
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+
+ test "if session-stored token matches a revoked OAuth token, does nothing", %{
+ conn: conn,
+ token: oauth_token
+ } do
+ Revoke.revoke(oauth_token)
+
+ ret_conn = OAuthPlug.call(conn, %{})
+ assert ret_conn == conn
+ end
+ end
end
diff --git a/test/pleroma/web/plugs/set_user_session_id_plug_test.exs b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
new file mode 100644
index 000000000..9814c80d8
--- /dev/null
+++ b/test/pleroma/web/plugs/set_user_session_id_plug_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Plugs.SetUserSessionIdPlugTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Helpers.AuthHelper
+ alias Pleroma.Web.Plugs.SetUserSessionIdPlug
+
+ setup %{conn: conn} do
+ session_opts = [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(session_opts))
+ |> fetch_session()
+
+ %{conn: conn}
+ end
+
+ test "doesn't do anything if the user isn't set", %{conn: conn} do
+ ret_conn = SetUserSessionIdPlug.call(conn, %{})
+
+ assert ret_conn == conn
+ end
+
+ test "sets session token basing on :token assign", %{conn: conn} do
+ %{user: user, token: oauth_token} = oauth_access(["read"])
+
+ ret_conn =
+ conn
+ |> assign(:user, user)
+ |> assign(:token, oauth_token)
+ |> SetUserSessionIdPlug.call(%{})
+
+ assert AuthHelper.get_session_token(ret_conn) == oauth_token.token
+ end
+end
From 152c43ac9e391a7db92013ef4b45a8dbc6db6049 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Tue, 23 Aug 2022 12:09:01 +0100
Subject: [PATCH 15/44] update mfm_parser
---
mix.exs | 2 +-
mix.lock | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/mix.exs b/mix.exs
index e7f491997..5312be47b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -193,7 +193,7 @@ defp deps do
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
- ref: "5054e0ba1ebcbd9a7916aec219528e3e58057241"},
+ ref: "48d0da81e060fdfb161696fc4e7e8e19190c8746"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
diff --git a/mix.lock b/mix.lock
index 1c3b550e7..e488648af 100644
--- a/mix.lock
+++ b/mix.lock
@@ -67,7 +67,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "5054e0ba1ebcbd9a7916aec219528e3e58057241", [ref: "5054e0ba1ebcbd9a7916aec219528e3e58057241"]},
+ "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "48d0da81e060fdfb161696fc4e7e8e19190c8746", [ref: "48d0da81e060fdfb161696fc4e7e8e19190c8746"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
@@ -110,6 +110,7 @@
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
+ "temple": {:git, "git@akkoma.dev:floatingghost/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"timex": {:hex, :timex, "3.7.8", "0e6e8bf7c0aba95f1e13204889b2446e7a5297b1c8e408f15ab58b2c8dc85f81", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8f3b8edc5faab5205d69e5255a1d64a83b190bab7f16baa78aefcb897cf81435"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
From 3cf8c1eb31844a2a46ba7f3b950a39068c555ae1 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Tue, 23 Aug 2022 12:13:35 +0100
Subject: [PATCH 16/44] use public temple dep
---
mix.exs | 2 +-
mix.lock | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/mix.exs b/mix.exs
index 5312be47b..2c2d63b01 100644
--- a/mix.exs
+++ b/mix.exs
@@ -193,7 +193,7 @@ defp deps do
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
- ref: "48d0da81e060fdfb161696fc4e7e8e19190c8746"},
+ ref: "51282dd6a784f4e75d6987ae3ceb91671e46dcfb"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
diff --git a/mix.lock b/mix.lock
index e488648af..9f9d5b035 100644
--- a/mix.lock
+++ b/mix.lock
@@ -67,7 +67,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "48d0da81e060fdfb161696fc4e7e8e19190c8746", [ref: "48d0da81e060fdfb161696fc4e7e8e19190c8746"]},
+ "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "51282dd6a784f4e75d6987ae3ceb91671e46dcfb", [ref: "51282dd6a784f4e75d6987ae3ceb91671e46dcfb"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
@@ -110,7 +110,7 @@
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
- "temple": {:git, "git@akkoma.dev:floatingghost/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
+ "temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
"timex": {:hex, :timex, "3.7.8", "0e6e8bf7c0aba95f1e13204889b2446e7a5297b1c8e408f15ab58b2c8dc85f81", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8f3b8edc5faab5205d69e5255a1d64a83b190bab7f16baa78aefcb897cf81435"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
From 9b6feb6657763113ad5092a58afd40967933509b Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Tue, 23 Aug 2022 16:10:19 +0100
Subject: [PATCH 17/44] add language tags
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 5debafaea..c3ead7fc1 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
*a smallish microblogging platform, aka the cooler pleroma*
+![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
+
## About
This is a fork of Pleroma, which is a microblogging server software that can federate (= exchange messages with) other servers that support ActivityPub. What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Akkoma will federate with all servers that implement ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
From fd7f4874baeaa68984cb41995efb8c8dee9f8627 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Wed, 24 Aug 2022 10:06:48 +0100
Subject: [PATCH 18/44] allow new mfm classes
---
mix.exs | 2 +-
mix.lock | 2 +-
priv/scrubbers/default.ex | 28 ++++++++++++++--------------
3 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/mix.exs b/mix.exs
index 2c2d63b01..f6cdac595 100644
--- a/mix.exs
+++ b/mix.exs
@@ -193,7 +193,7 @@ defp deps do
git: "https://akkoma.dev/AkkomaGang/elasticsearch-elixir.git", ref: "main"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
- ref: "51282dd6a784f4e75d6987ae3ceb91671e46dcfb"},
+ ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"},
# indirect dependency version override
{:plug, "~> 1.10.4", override: true},
diff --git a/mix.lock b/mix.lock
index 9f9d5b035..bda77cf5a 100644
--- a/mix.lock
+++ b/mix.lock
@@ -67,7 +67,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "51282dd6a784f4e75d6987ae3ceb91671e46dcfb", [ref: "51282dd6a784f4e75d6987ae3ceb91671e46dcfb"]},
+ "mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "912fba81152d4d572e457fd5427f9875b2bc3dbe", [ref: "912fba81152d4d572e457fd5427f9875b2bc3dbe"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
diff --git a/priv/scrubbers/default.ex b/priv/scrubbers/default.ex
index 68ac06e32..950b6c21e 100644
--- a/priv/scrubbers/default.ex
+++ b/priv/scrubbers/default.ex
@@ -62,20 +62,20 @@ defmodule Pleroma.HTML.Scrubber.Default do
"h-card",
"quote-inline",
"mfm",
- "_mfm_tada_",
- "_mfm_jelly_",
- "_mfm_twitch_",
- "_mfm_shake_",
- "_mfm_spin_",
- "_mfm_jump_",
- "_mfm_bounce_",
- "_mfm_flip_",
- "_mfm_x2_",
- "_mfm_x3_",
- "_mfm_x4_",
- "_mfm_blur_",
- "_mfm_rainbow_",
- "_mfm_rotate_"
+ "mfm _mfm_tada_",
+ "mfm _mfm_jelly_",
+ "mfm _mfm_twitch_",
+ "mfm _mfm_shake_",
+ "mfm _mfm_spin_",
+ "mfm _mfm_jump_",
+ "mfm _mfm_bounce_",
+ "mfm _mfm_flip_",
+ "mfm _mfm_x2_",
+ "mfm _mfm_x3_",
+ "mfm _mfm_x4_",
+ "mfm _mfm_blur_",
+ "mfm _mfm_rainbow_",
+ "mfm _mfm_rotate_"
])
Meta.allow_tag_with_these_attributes(:span, [
From 92ba2802fb0d71a0bbca676ecc3af40c0a27db53 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Wed, 24 Aug 2022 14:36:33 +0000
Subject: [PATCH 19/44] generate-keys-at-registration-time (#181)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/181
---
lib/mix/pleroma.ex | 11 +++++-
lib/mix/tasks/pleroma/user.ex | 39 +++++++++++++++++++
lib/pleroma/user.ex | 7 ++++
test/pleroma/user_test.exs | 3 +-
.../article_note_page_validator_test.exs | 2 +-
5 files changed, 59 insertions(+), 3 deletions(-)
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index f4a6bcf63..6431f0a1c 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -23,7 +23,15 @@ def start_pleroma do
Pleroma.Config.Oban.warn()
Pleroma.Application.limiters_setup()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
- Finch.start_link(name: MyFinch)
+
+ proxy_url = Pleroma.Config.get([:http, :proxy_url])
+ proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
+
+ finch_config =
+ [:http, :adapter]
+ |> Pleroma.Config.get([])
+ |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
+ |> Keyword.put(:name, MyFinch)
unless System.get_env("DEBUG") do
Logger.remove_backend(:console)
@@ -45,6 +53,7 @@ def start_pleroma do
Pleroma.Emoji,
{Pleroma.Config.TransferTask, false},
Pleroma.Web.Endpoint,
+ {Finch, finch_config},
{Oban, oban_config},
{Majic.Pool,
[name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index d2d416655..f420d68bb 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -258,6 +258,25 @@ def run(["untag", nickname | tags]) do
end
end
+ def run(["refetch_public_keys"]) do
+ start_pleroma()
+
+ Pleroma.User.Query.build(%{
+ external: true,
+ is_active: true
+ })
+ |> refetch_public_keys()
+ end
+
+ def run(["refetch_public_keys" | rest]) do
+ start_pleroma()
+
+ Pleroma.User.Query.build(%{
+ ap_id: rest
+ })
+ |> refetch_public_keys()
+ end
+
def run(["invite" | rest]) do
{options, [], []} =
OptionParser.parse(rest,
@@ -519,6 +538,26 @@ def run(["fix_follow_state", local_user, remote_user]) do
end
end
+ defp refetch_public_keys(query) do
+ query
+ |> Pleroma.Repo.chunk_stream(50, :batches)
+ |> Stream.each(fn users ->
+ users
+ |> Enum.each(fn user ->
+ IO.puts("Re-Resolving: #{user.ap_id}")
+
+ with {:ok, user} <- Pleroma.User.fetch_by_ap_id(user.ap_id),
+ changeset <- Pleroma.User.update_changeset(user),
+ {:ok, _user} <- Pleroma.User.update_and_set_cache(changeset) do
+ :ok
+ else
+ error -> IO.puts("Could not resolve: #{user.ap_id}, #{inspect(error)}")
+ end
+ end)
+ end)
+ |> Stream.run()
+ end
+
defp set_moderator(user, value) do
{:ok, user} =
user
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 2a1b5af94..4383f8f53 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -681,6 +681,7 @@ def register_changeset_ldap(struct, params = %{password: password})
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
+ |> put_keys()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
end
@@ -740,6 +741,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
+ |> put_keys()
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
@@ -755,6 +757,11 @@ def maybe_validate_required_email(changeset, _) do
end
end
+ def put_keys(changeset) do
+ {:ok, pem} = Keys.generate_rsa_pem()
+ put_change(changeset, :keys, pem)
+ end
+
def put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 67136e95b..645622e43 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -620,13 +620,14 @@ test "it blocks blacklisted email domains" do
assert changeset.valid?
end
- test "it sets the password_hash and ap_id" do
+ test "it sets the password_hash, ap_id and PEM key" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
+ assert is_binary(changeset.changes[:keys])
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index c766414a6..1d73d6765 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -115,7 +115,7 @@ test "a misskey MFM status with a content field should work and be linked", _ do
assert content =~ "@oops_not_a_mention"
assert content =~
- "mfm goes here aaa"
+ "mfm goes here aaa"
end
test "a misskey MFM status with a _misskey_content field should work and be linked", _ do
From 017b50550b88c9320ed111a54fd770d19dc1416d Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Wed, 24 Aug 2022 15:38:02 +0100
Subject: [PATCH 20/44] add changelog entry
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41b352f21..3966bb10b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Compatibility with latest meilisearch
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
- Elasticsearch returning likes and repeats, displaying as posts
+- Ensure key generation happens at registration-time to prevent potential race-conditions
### Removed
- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
From 618cf7ff7f60f9a81a5d85160416107032ad7b34 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Thu, 25 Aug 2022 14:37:51 +0000
Subject: [PATCH 21/44] reuse valid oauth tokens (#182)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/182
---
lib/pleroma/helpers/auth_helper.ex | 13 ++
.../controllers/auth_controller.ex | 3 +-
lib/pleroma/web/o_auth/authorization.ex | 5 +
lib/pleroma/web/o_auth/o_auth_controller.ex | 32 ++++-
lib/pleroma/web/o_auth/token.ex | 18 +++
lib/pleroma/web/o_auth/token/query.ex | 13 ++
.../web/o_auth/o_auth_controller_test.exs | 123 ++++++++++++++++++
7 files changed, 202 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/helpers/auth_helper.ex b/lib/pleroma/helpers/auth_helper.ex
index 13e4c8158..37765da4d 100644
--- a/lib/pleroma/helpers/auth_helper.ex
+++ b/lib/pleroma/helpers/auth_helper.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Helpers.AuthHelper do
import Plug.Conn
@oauth_token_session_key :oauth_token
+ @oauth_user_session_key :oauth_user
@doc """
Skips OAuth permissions (scopes) checks, assigns nil `:token`.
@@ -43,4 +44,16 @@ def put_session_token(%Conn{} = conn, token) when is_binary(token) do
def delete_session_token(%Conn{} = conn) do
delete_session(conn, @oauth_token_session_key)
end
+
+ def put_session_user(%Conn{} = conn, user) do
+ put_session(conn, @oauth_user_session_key, user)
+ end
+
+ def delete_session_user(%Conn{} = conn) do
+ delete_session(conn, @oauth_user_session_key)
+ end
+
+ def get_session_user(%Conn{} = conn) do
+ get_session(conn, @oauth_user_session_key)
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index 4920d65da..a9ccaa982 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -27,7 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
def login(conn, %{"code" => auth_token} = params) do
with {:ok, app} <- local_mastofe_app(),
{:ok, auth} <- Authorization.get_by_token(app, auth_token),
- {:ok, oauth_token} <- Token.exchange_token(app, auth) do
+ %User{} = user <- User.get_cached_by_id(auth.user_id),
+ {:ok, oauth_token} <- Token.get_or_exchange_token(auth, app, user) do
redirect_to =
conn
|> local_mastodon_post_login_path()
diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex
index e0ecb0f4f..e56704164 100644
--- a/lib/pleroma/web/o_auth/authorization.ex
+++ b/lib/pleroma/web/o_auth/authorization.ex
@@ -94,4 +94,9 @@ def get_by_token(%App{id: app_id} = _app, token) do
from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token)
|> Repo.find_resource()
end
+
+ def get_preeexisting_by_app_and_user(%App{id: app_id} = _app, %User{id: user_id} = _user) do
+ from(t in __MODULE__, where: t.app_id == ^app_id and t.user_id == ^user_id, limit: 1)
+ |> Repo.find_resource()
+ end
end
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
index 358120fe6..455af11d7 100644
--- a/lib/pleroma/web/o_auth/o_auth_controller.ex
+++ b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -59,18 +59,39 @@ def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, %{"force_login" =>
# after user already authorized to MastodonFE.
# So we have to check client and token.
def authorize(
- %Plug.Conn{assigns: %{token: %Token{} = token}} = conn,
+ %Plug.Conn{assigns: %{token: %Token{} = token, user: %User{} = user}} = conn,
%{"client_id" => client_id} = params
) do
with %Token{} = t <- Repo.get_by(Token, token: token.token) |> Repo.preload(:app),
^client_id <- t.app.client_id do
handle_existing_authorization(conn, params)
+ else
+ _ ->
+ maybe_reuse_token(conn, params, user.id)
+ end
+ end
+
+ def authorize(%Plug.Conn{} = conn, params) do
+ # if we have a user in the session, attempt to authenticate as them
+ # otherwise show the login form
+ maybe_reuse_token(conn, params, AuthHelper.get_session_user(conn))
+ end
+
+ defp maybe_reuse_token(conn, params, user_id) when is_binary(user_id) do
+ with %User{} = user <- User.get_cached_by_id(user_id),
+ %App{} = app <- Repo.get_by(App, client_id: params["client_id"]),
+ {:ok, %Token{} = token} <- Token.get_preeexisting_by_app_and_user(app, user),
+ {:ok, %Authorization{} = auth} <-
+ Authorization.get_preeexisting_by_app_and_user(app, user) do
+ conn
+ |> assign(:token, token)
+ |> after_create_authorization(auth, %{"authorization" => params})
else
_ -> do_authorize(conn, params)
end
end
- def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params)
+ defp maybe_reuse_token(conn, params, _user), do: do_authorize(conn, params)
defp do_authorize(%Plug.Conn{} = conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
@@ -148,7 +169,9 @@ def create_authorization(%Plug.Conn{assigns: %{user: %User{} = user}} = conn, pa
def create_authorization(%Plug.Conn{} = conn, %{"authorization" => _} = params, opts) do
with {:ok, auth, user} <- do_create_authorization(conn, params, opts[:user]),
{:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)} do
- after_create_authorization(conn, auth, params)
+ conn
+ |> AuthHelper.put_session_user(user.id)
+ |> after_create_authorization(auth, params)
else
error ->
handle_create_authorization_error(conn, error, params)
@@ -269,7 +292,7 @@ def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"}
fixed_token = Token.Utils.fix_padding(params["code"]),
{:ok, auth} <- Authorization.get_by_token(app, fixed_token),
%User{} = user <- User.get_cached_by_id(auth.user_id),
- {:ok, token} <- Token.exchange_token(app, auth) do
+ {:ok, token} <- Token.get_or_exchange_token(auth, app, user) do
after_token_exchange(conn, %{user: user, token: token})
else
error ->
@@ -321,6 +344,7 @@ def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
conn
|> AuthHelper.put_session_token(token.token)
+ |> AuthHelper.put_session_user(token.user_id)
|> json(OAuthView.render("token.json", view_params))
end
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
index 9d69e9db4..c9398aeaa 100644
--- a/lib/pleroma/web/o_auth/token.ex
+++ b/lib/pleroma/web/o_auth/token.ex
@@ -70,6 +70,16 @@ def exchange_token(app, auth) do
end
end
+ def get_preeexisting_by_app_and_user(app, user) do
+ Query.get_by_app(app.id)
+ |> Query.get_by_user(user.id)
+ |> Query.get_unexpired()
+ |> Query.preload([:user])
+ |> Query.sort_by_inserted_at()
+ |> Query.limit(1)
+ |> Repo.find_resource()
+ end
+
defp put_token(changeset) do
changeset
|> change(%{token: Token.Utils.generate_token()})
@@ -86,6 +96,14 @@ defp put_refresh_token(changeset, attrs) do
|> unique_constraint(:refresh_token)
end
+ def get_or_exchange_token(%Authorization{} = auth, %App{} = app, %User{} = user) do
+ if auth.used do
+ get_preeexisting_by_app_and_user(app, user)
+ else
+ exchange_token(app, auth)
+ end
+ end
+
defp put_valid_until(changeset, attrs) do
valid_until =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan()))
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
index d16a759d8..662e7856d 100644
--- a/lib/pleroma/web/o_auth/token/query.ex
+++ b/lib/pleroma/web/o_auth/token/query.ex
@@ -38,6 +38,19 @@ def get_by_user(query \\ Token, user_id) do
from(q in query, where: q.user_id == ^user_id)
end
+ def get_unexpired(query) do
+ now = NaiveDateTime.utc_now()
+ from(q in query, where: q.valid_until > ^now)
+ end
+
+ def limit(query, limit) do
+ from(q in query, limit: ^limit)
+ end
+
+ def sort_by_inserted_at(query) do
+ from(q in query, order_by: [desc: :updated_at])
+ end
+
@spec preload(query, any) :: query
def preload(query \\ Token, assoc_preload \\ [])
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index 0fdd5b8e9..5a1258ec3 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -494,6 +494,129 @@ test "renders authentication page if user is already authenticated but user requ
assert html_response(conn, 200) =~ ~s(type="submit")
end
+ test "allows access if the user has a prior authorization but is authenticated with another client",
+ %{
+ app: app,
+ conn: conn
+ } do
+ user = insert(:user)
+ token = insert(:oauth_token, app: app, user: user)
+
+ other_app = insert(:oauth_app, redirect_uris: "https://other_redirect.url")
+ authorization = insert(:oauth_authorization, user: user, app: other_app)
+ _reusable_token = insert(:oauth_token, app: other_app, user: user)
+
+ conn =
+ conn
+ |> AuthHelper.put_session_token(token.token)
+ |> AuthHelper.put_session_user(user.id)
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert URI.decode(redirected_to(conn)) ==
+ "https://other_redirect.url?code=#{authorization.token}"
+ end
+
+ test "renders login page if the user has an authorization but no token",
+ %{
+ app: app,
+ conn: conn
+ } do
+ user = insert(:user)
+ token = insert(:oauth_token, app: app, user: user)
+
+ other_app = insert(:oauth_app, redirect_uris: "https://other_redirect.url")
+ _authorization = insert(:oauth_authorization, user: user, app: other_app)
+
+ conn =
+ conn
+ |> AuthHelper.put_session_token(token.token)
+ |> AuthHelper.put_session_user(user.id)
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
+ test "does not reuse other people's tokens",
+ %{
+ app: app,
+ conn: conn
+ } do
+ user = insert(:user)
+ other_user = insert(:user)
+ token = insert(:oauth_token, app: app, user: user)
+
+ other_app = insert(:oauth_app, redirect_uris: "https://other_redirect.url")
+ _authorization = insert(:oauth_authorization, user: other_user, app: other_app)
+ _reusable_token = insert(:oauth_token, app: other_app, user: other_user)
+
+ conn =
+ conn
+ |> AuthHelper.put_session_token(token.token)
+ |> AuthHelper.put_session_user(user.id)
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
+ test "does not reuse expired tokens",
+ %{
+ app: app,
+ conn: conn
+ } do
+ user = insert(:user)
+ token = insert(:oauth_token, app: app, user: user)
+
+ other_app = insert(:oauth_app, redirect_uris: "https://other_redirect.url")
+ _authorization = insert(:oauth_authorization, user: user, app: other_app)
+
+ _reusable_token =
+ insert(:oauth_token,
+ app: other_app,
+ user: user,
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -100)
+ )
+
+ conn =
+ conn
+ |> AuthHelper.put_session_token(token.token)
+ |> AuthHelper.put_session_user(user.id)
+ |> get(
+ "/oauth/authorize",
+ %{
+ "response_type" => "code",
+ "client_id" => other_app.client_id,
+ "redirect_uri" => OAuthController.default_redirect_uri(other_app),
+ "scope" => "read"
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
From e4f2251e0f5744de66b9b3bee2a0086bc7ab2bb1 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Thu, 25 Aug 2022 16:11:21 +0000
Subject: [PATCH 22/44] Add support for setting language in instance metadata
(#183)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/183
---
CHANGELOG.md | 2 ++
config/config.exs | 1 +
config/description.exs | 10 ++++++++++
lib/pleroma/web/mastodon_api/views/instance_view.ex | 2 +-
.../controllers/instance_controller_test.exs | 5 +++--
5 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3966bb10b..df34d2bb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- support for fedibird-fe, and non-breaking API parity for it to function
+- support for setting instance languages in metadata
+- support for reusing oauth tokens, and not requiring new authorizations
### Changed
- MFM parsing is now done on the backend by a modified version of ilja's parser -> https://akkoma.dev/AkkomaGang/mfm-parser
diff --git a/config/config.exs b/config/config.exs
index 83977da19..ec82e872a 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -197,6 +197,7 @@
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
+ languages: ["en"],
poll_limits: %{
max_options: 20,
max_option_chars: 200,
diff --git a/config/description.exs b/config/description.exs
index b70982cd2..61ef8f449 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -509,6 +509,16 @@
"Pleroma"
]
},
+ %{
+ key: :languages,
+ type: {:list, :string},
+ description: "Languages the instance uses",
+ suggestions: [
+ "en",
+ "ja",
+ "fr"
+ ]
+ },
%{
key: :email,
label: "Admin Email Address",
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 4cfa8d85c..7ae357e23 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -26,7 +26,7 @@ def render("show.json", _) do
thumbnail:
URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
|> to_string,
- languages: ["en"],
+ languages: Keyword.get(instance, :languages, ["en"]),
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
# Extra (not present in Mastodon):
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
index 90801a90a..bc3d35819 100644
--- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -10,10 +10,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
import Pleroma.Factory
test "get instance information", %{conn: conn} do
+ clear_config([:instance, :languages], ["en", "ja"])
conn = get(conn, "/api/v1/instance")
assert result = json_response_and_validate_schema(conn, 200)
-
email = Pleroma.Config.get([:instance, :email])
+
thumbnail = Pleroma.Web.Endpoint.url() <> Pleroma.Config.get([:instance, :instance_thumbnail])
background = Pleroma.Web.Endpoint.url() <> Pleroma.Config.get([:instance, :background_image])
@@ -29,7 +30,7 @@ test "get instance information", %{conn: conn} do
},
"stats" => _,
"thumbnail" => from_config_thumbnail,
- "languages" => _,
+ "languages" => ["en", "ja"],
"registrations" => _,
"approval_required" => _,
"poll_limits" => _,
From db7ad08d1e2d4c59b52a41bb0790c073fda1649f Mon Sep 17 00:00:00 2001
From: Norm
Date: Thu, 25 Aug 2022 18:30:19 +0000
Subject: [PATCH 23/44] Update min elixir version in mix.exs to 1.12
The install docs already mention 1.12 as the minimum supported version, so this should also be reflected in `mix.exs`.
---
mix.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mix.exs b/mix.exs
index e7f491997..56c854021 100644
--- a/mix.exs
+++ b/mix.exs
@@ -5,7 +5,7 @@ def project do
[
app: :pleroma,
version: version("3.1.0"),
- elixir: "~> 1.9",
+ elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
From 85137f591f95169d385d690c48dcbeccb1306058 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Sat, 27 Aug 2022 11:57:57 +0100
Subject: [PATCH 24/44] Add ability to obfuscate domains in MRF transparency
---
CHANGELOG.md | 1 +
config/config.exs | 3 +-
docs/docs/configuration/cheatsheet.md | 1 +
lib/pleroma/web/activity_pub/mrf.ex | 10 +++++
.../web/activity_pub/mrf/simple_policy.ex | 31 +++++++++++++++-
.../activity_pub/mrf/simple_policy_test.exs | 37 +++++++++++++++++++
6 files changed, 80 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df34d2bb6..183a60d10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- support for fedibird-fe, and non-breaking API parity for it to function
- support for setting instance languages in metadata
- support for reusing oauth tokens, and not requiring new authorizations
+- the ability to obfuscate domains in your MRF descriptions
### Changed
- MFM parsing is now done on the backend by a modified version of ilja's parser -> https://akkoma.dev/AkkomaGang/mfm-parser
diff --git a/config/config.exs b/config/config.exs
index ec82e872a..5ae7a33a2 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -794,7 +794,8 @@
config :pleroma, :mrf,
policies: [Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy, Pleroma.Web.ActivityPub.MRF.TagPolicy],
transparency: true,
- transparency_exclusions: []
+ transparency_exclusions: [],
+ transparency_obfuscate_domains: []
config :ex_aws, http_client: Pleroma.HTTP.ExAws
diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md
index 8fa188de1..a29db208c 100644
--- a/docs/docs/configuration/cheatsheet.md
+++ b/docs/docs/configuration/cheatsheet.md
@@ -120,6 +120,7 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
+* `transparency_obfuscate_domains`: Show domains with `*` in the middle, to censor them if needed. For example, `ridingho.me` will show as `rid*****.me`
## Federation
### MRF policies
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
index bd6f6777f..5606dac83 100644
--- a/lib/pleroma/web/activity_pub/mrf.ex
+++ b/lib/pleroma/web/activity_pub/mrf.ex
@@ -41,6 +41,16 @@ defmodule Pleroma.Web.ActivityPub.MRF do
suggestions: [
"exclusion.com"
]
+ },
+ %{
+ key: :transparency_obfuscate_domains,
+ label: "MRF domain obfuscation",
+ type: {:list, :string},
+ description:
+ "Obfuscate domains in MRF transparency. This is useful if the domain you're blocking contains words you don't want displayed, but still want to disclose the MRF settings.",
+ suggestions: [
+ "badword.com"
+ ]
}
]
}
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index c631cc85f..415c5d2dd 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -256,10 +256,35 @@ def filter(object) when is_binary(object) do
def filter(object), do: {:ok, object}
+ defp obfuscate(string) when is_binary(string) do
+ string
+ |> to_charlist()
+ |> Enum.with_index()
+ |> Enum.map(fn
+ {?., _index} ->
+ ?.
+
+ {char, index} ->
+ if 3 <= index && index < String.length(string) - 3, do: ?*, else: char
+ end)
+ |> to_string()
+ end
+
+ defp maybe_obfuscate(host, obfuscations) do
+ if MRF.subdomain_match?(obfuscations, host) do
+ obfuscate(host)
+ else
+ host
+ end
+ end
+
@impl true
def describe do
exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
+ obfuscations =
+ Config.get([:mrf, :transparency_obfuscate_domains], []) |> MRF.subdomains_regex()
+
mrf_simple_excluded =
Config.get(:mrf_simple)
|> Enum.map(fn {rule, instances} ->
@@ -269,7 +294,7 @@ def describe do
mrf_simple =
mrf_simple_excluded
|> Enum.map(fn {rule, instances} ->
- {rule, Enum.map(instances, fn {host, _} -> host end)}
+ {rule, Enum.map(instances, fn {host, _} -> maybe_obfuscate(host, obfuscations) end)}
end)
|> Map.new()
@@ -286,7 +311,9 @@ def describe do
|> Enum.map(fn {rule, instances} ->
instances =
instances
- |> Enum.map(fn {host, reason} -> {host, %{"reason" => reason}} end)
+ |> Enum.map(fn {host, reason} ->
+ {maybe_obfuscate(host, obfuscations), %{"reason" => reason}}
+ end)
|> Map.new()
{rule, instances}
diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
index 0a0f51bdb..0569bfed3 100644
--- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs
@@ -216,6 +216,43 @@ test "has a matching host but only as:Public in to" do
end
end
+ describe "describe/1" do
+ test "returns a description of the policy" do
+ clear_config([:mrf_simple, :reject], [
+ {"remote.instance", "did not give my catboy a burg"}
+ ])
+
+ assert {:ok, %{mrf_simple: %{reject: ["remote.instance"]}}} = SimplePolicy.describe()
+ end
+
+ test "excludes domains listed in :transparency_exclusions" do
+ clear_config([:mrf, :transparency_exclusions], [{"remote.instance", ":("}])
+
+ clear_config([:mrf_simple, :reject], [
+ {"remote.instance", "did not give my catboy a burg"}
+ ])
+
+ {:ok, description} = SimplePolicy.describe()
+ assert %{mrf_simple: %{reject: []}} = description
+ assert description[:mrf_simple_info][:reject] == nil
+ end
+
+ test "obfuscates domains listed in :transparency_obfuscate_domains" do
+ clear_config([:mrf, :transparency_obfuscate_domains], ["remote.instance", "a.b"])
+
+ clear_config([:mrf_simple, :reject], [
+ {"remote.instance", "did not give my catboy a burg"},
+ {"a.b", "spam-poked me on facebook in 2006"}
+ ])
+
+ assert {:ok,
+ %{
+ mrf_simple: %{reject: ["rem***.*****nce", "a.b"]},
+ mrf_simple_info: %{reject: %{"rem***.*****nce" => %{}}}
+ }} = SimplePolicy.describe()
+ end
+ end
+
defp build_ftl_actor_and_message do
actor = insert(:user)
From 772c209914d5cbfd4f763edc266d0f1541f656f8 Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Sat, 27 Aug 2022 18:05:48 +0000
Subject: [PATCH 25/44] GTS: cherry-picks and collection usage (#186)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3725?commit_id=61254111e59f02118cad15de49d1e0704c07030e
what is this, a yoink of a yoink? good times
Co-authored-by: Hélène
Co-authored-by: FloatingGhost
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/186
---
lib/pleroma/collections/fetcher.ex | 38 +++++++-----
.../article_note_page_validator.ex | 19 ++----
.../object_validators/common_fixes.ex | 29 ++++++++-
.../create_generic_validator.ex | 3 +-
.../web/activity_pub/transmogrifier.ex | 26 +-------
lib/pleroma/web/plugs/http_signature_plug.ex | 8 ++-
.../collections/collections_fetcher_test.exs | 59 +++++++++++++++++--
.../activity_pub_controller_test.exs | 11 ++++
.../article_note_page_validator_test.exs | 13 ++++
.../transmogrifier/note_handling_test.exs | 1 -
.../web/plugs/http_signature_plug_test.exs | 6 +-
test/support/http_request_mock.ex | 9 +++
12 files changed, 155 insertions(+), 67 deletions(-)
diff --git a/lib/pleroma/collections/fetcher.ex b/lib/pleroma/collections/fetcher.ex
index 0c81f0b56..ab69f4b84 100644
--- a/lib/pleroma/collections/fetcher.ex
+++ b/lib/pleroma/collections/fetcher.ex
@@ -11,10 +11,7 @@ defmodule Akkoma.Collections.Fetcher do
alias Pleroma.Config
require Logger
- def fetch_collection_by_ap_id(ap_id) when is_binary(ap_id) do
- fetch_collection(ap_id)
- end
-
+ @spec fetch_collection(String.t() | map()) :: {:ok, [Pleroma.Object.t()]} | {:error, any()}
def fetch_collection(ap_id) when is_binary(ap_id) do
with {:ok, page} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
{:ok, objects_from_collection(page)}
@@ -26,7 +23,7 @@ def fetch_collection(ap_id) when is_binary(ap_id) do
end
def fetch_collection(%{"type" => type} = page)
- when type in ["Collection", "OrderedCollection"] do
+ when type in ["Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage"] do
{:ok, objects_from_collection(page)}
end
@@ -38,12 +35,13 @@ defp items_in_page(%{"type" => type, "items" => items})
when is_list(items) and type in ["Collection", "CollectionPage"],
do: items
- defp objects_from_collection(%{"type" => "OrderedCollection", "orderedItems" => items})
- when is_list(items),
- do: items
+ defp objects_from_collection(%{"type" => type, "orderedItems" => items} = page)
+ when is_list(items) and type in ["OrderedCollection", "OrderedCollectionPage"],
+ do: maybe_next_page(page, items)
- defp objects_from_collection(%{"type" => "Collection", "items" => items}) when is_list(items),
- do: items
+ defp objects_from_collection(%{"type" => type, "items" => items} = page)
+ when is_list(items) and type in ["Collection", "CollectionPage"],
+ do: maybe_next_page(page, items)
defp objects_from_collection(%{"type" => type, "first" => first})
when is_binary(first) and type in ["Collection", "OrderedCollection"] do
@@ -55,17 +53,27 @@ defp objects_from_collection(%{"type" => type, "first" => %{"id" => id}})
fetch_page_items(id)
end
+ defp objects_from_collection(_page), do: []
+
defp fetch_page_items(id, items \\ []) do
if Enum.count(items) >= Config.get([:activitypub, :max_collection_objects]) do
items
else
- {:ok, page} = Fetcher.fetch_and_contain_remote_object_from_id(id)
- objects = items_in_page(page)
+ with {:ok, page} <- Fetcher.fetch_and_contain_remote_object_from_id(id) do
+ objects = items_in_page(page)
- if Enum.count(objects) > 0 do
- maybe_next_page(page, items ++ objects)
+ if Enum.count(objects) > 0 do
+ maybe_next_page(page, items ++ objects)
+ else
+ items
+ end
else
- items
+ {:error, "Object has been deleted"} ->
+ items
+
+ {:error, error} ->
+ Logger.error("Could not fetch page #{id} - #{inspect(error)}")
+ {:error, error}
end
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
index 28053ea3a..55323bc2e 100644
--- a/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/article_note_page_validator.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
use Ecto.Schema
alias Pleroma.User
alias Pleroma.EctoType.ActivityPub.ObjectValidators
- alias Pleroma.Object.Fetcher
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -58,19 +57,10 @@ defp fix_tag(%{"tag" => tag} = data) when is_list(tag), do: data
defp fix_tag(%{"tag" => tag} = data) when is_map(tag), do: Map.put(data, "tag", [tag])
defp fix_tag(data), do: Map.drop(data, ["tag"])
- defp fix_replies(%{"replies" => %{"first" => %{"items" => replies}}} = data)
- when is_list(replies),
- do: Map.put(data, "replies", replies)
-
- defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
- do: Map.put(data, "replies", replies)
-
- defp fix_replies(%{"replies" => replies} = data) when is_bitstring(replies),
- do: Map.drop(data, ["replies"])
+ defp fix_replies(%{"replies" => replies} = data) when is_list(replies), do: data
defp fix_replies(%{"replies" => %{"first" => first}} = data) do
- with {:ok, %{"orderedItems" => replies}} <-
- Fetcher.fetch_and_contain_remote_object_from_id(first) do
+ with {:ok, replies} <- Akkoma.Collections.Fetcher.fetch_collection(first) do
Map.put(data, "replies", replies)
else
{:error, _} ->
@@ -79,7 +69,10 @@ defp fix_replies(%{"replies" => %{"first" => first}} = data) do
end
end
- defp fix_replies(data), do: data
+ defp fix_replies(%{"replies" => %{"items" => replies}} = data) when is_list(replies),
+ do: Map.put(data, "replies", replies)
+
+ defp fix_replies(data), do: Map.delete(data, "replies")
defp remote_mention_resolver(
%{"id" => ap_id, "tag" => tags},
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
index 779c8b622..6fa2bbb99 100644
--- a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -7,8 +7,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
+ require Pleroma.Constants
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
@@ -32,7 +32,7 @@ def fix_object_defaults(data) do
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
- |> Transmogrifier.fix_implicit_addressing(follower_collection)
+ |> fix_implicit_addressing(follower_collection)
end
def fix_activity_addressing(activity) do
@@ -43,7 +43,7 @@ def fix_activity_addressing(activity) do
|> cast_and_filter_recipients("cc", follower_collection)
|> cast_and_filter_recipients("bto", follower_collection)
|> cast_and_filter_recipients("bcc", follower_collection)
- |> Transmogrifier.fix_implicit_addressing(follower_collection)
+ |> fix_implicit_addressing(follower_collection)
end
def fix_actor(data) do
@@ -73,4 +73,27 @@ def fix_object_action_recipients(data, %Object{data: %{"actor" => actor}}) do
Map.put(data, "to", to)
end
+
+ # if as:Public is addressed, then make sure the followers collection is also addressed
+ # so that the activities will be delivered to local users.
+ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
+ recipients = to ++ cc
+
+ if followers_collection not in recipients do
+ cond do
+ Pleroma.Constants.as_public() in cc ->
+ to = to ++ [followers_collection]
+ Map.put(object, "to", to)
+
+ Pleroma.Constants.as_public() in to ->
+ cc = cc ++ [followers_collection]
+ Map.put(object, "cc", cc)
+
+ true ->
+ object
+ end
+ else
+ object
+ end
+ end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
index 803b5d5a1..d868c3915 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/create_generic_validator.ex
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
- alias Pleroma.Web.ActivityPub.Transmogrifier
import Ecto.Changeset
@@ -67,7 +66,7 @@ defp fix_addressing(data, object) do
|> CommonFixes.cast_and_filter_recipients("cc", follower_collection, object["cc"])
|> CommonFixes.cast_and_filter_recipients("bto", follower_collection, object["bto"])
|> CommonFixes.cast_and_filter_recipients("bcc", follower_collection, object["bcc"])
- |> Transmogrifier.fix_implicit_addressing(follower_collection)
+ |> CommonFixes.fix_implicit_addressing(follower_collection)
end
def fix(data, meta) do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index d2077967c..8ec4b0fec 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.Federator
alias Pleroma.Workers.TransmogrifierWorker
@@ -95,29 +96,6 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collect
|> Map.put("cc", final_cc)
end
- # if as:Public is addressed, then make sure the followers collection is also addressed
- # so that the activities will be delivered to local users.
- def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
- recipients = to ++ cc
-
- if followers_collection not in recipients do
- cond do
- Pleroma.Constants.as_public() in cc ->
- to = to ++ [followers_collection]
- Map.put(object, "to", to)
-
- Pleroma.Constants.as_public() in to ->
- cc = cc ++ [followers_collection]
- Map.put(object, "cc", cc)
-
- true ->
- object
- end
- else
- object
- end
- end
-
def fix_addressing(object) do
{:ok, %User{follower_address: follower_collection}} =
object
@@ -130,7 +108,7 @@ def fix_addressing(object) do
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_explicit_addressing(follower_collection)
- |> fix_implicit_addressing(follower_collection)
+ |> CommonFixes.fix_implicit_addressing(follower_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
diff --git a/lib/pleroma/web/plugs/http_signature_plug.ex b/lib/pleroma/web/plugs/http_signature_plug.ex
index cfee392c8..c906a4eec 100644
--- a/lib/pleroma/web/plugs/http_signature_plug.ex
+++ b/lib/pleroma/web/plugs/http_signature_plug.ex
@@ -27,11 +27,11 @@ def call(conn, _opts) do
end
end
- def route_aliases(%{path_info: ["objects", id]}) do
+ def route_aliases(%{path_info: ["objects", id], query_string: query_string}) do
ap_id = Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, id)
with %Activity{} = activity <- Activity.get_by_object_ap_id_with_object(ap_id) do
- ["/notice/#{activity.id}"]
+ ["/notice/#{activity.id}", "/notice/#{activity.id}?#{query_string}"]
else
_ -> []
end
@@ -64,7 +64,9 @@ defp maybe_assign_valid_signature(conn) do
if has_signature_header?(conn) do
# set (request-target) header to the appropriate value
# we also replace the digest header with the one we computed
- possible_paths = route_aliases(conn) ++ [conn.request_path]
+ possible_paths =
+ route_aliases(conn) ++ [conn.request_path, conn.request_path <> "?#{conn.query_string}"]
+
assign_valid_signature_on_route_aliases(conn, possible_paths)
else
Logger.debug("No signature header!")
diff --git a/test/pleroma/collections/collections_fetcher_test.exs b/test/pleroma/collections/collections_fetcher_test.exs
index b9f84f5c4..7a582a3d7 100644
--- a/test/pleroma/collections/collections_fetcher_test.exs
+++ b/test/pleroma/collections/collections_fetcher_test.exs
@@ -30,7 +30,7 @@ test "it should extract items from an embedded array in a Collection" do
}
end)
- {:ok, objects} = Fetcher.fetch_collection_by_ap_id(ap_id)
+ {:ok, objects} = Fetcher.fetch_collection(ap_id)
assert [%{"type" => "Create"}, %{"type" => "Like"}] = objects
end
@@ -53,7 +53,7 @@ test "it should extract items from an embedded array in an OrderedCollection" do
}
end)
- {:ok, objects} = Fetcher.fetch_collection_by_ap_id(ap_id)
+ {:ok, objects} = Fetcher.fetch_collection(ap_id)
assert [%{"type" => "Create"}, %{"type" => "Like"}] = objects
end
@@ -106,7 +106,7 @@ test "it should extract items from an referenced first page in a Collection" do
}
end)
- {:ok, objects} = Fetcher.fetch_collection_by_ap_id(ap_id)
+ {:ok, objects} = Fetcher.fetch_collection(ap_id)
assert [%{"type" => "Create"}, %{"type" => "Like"}] = objects
end
@@ -161,7 +161,58 @@ test "it should stop fetching when we hit :max_collection_objects" do
}
end)
- {:ok, objects} = Fetcher.fetch_collection_by_ap_id(ap_id)
+ {:ok, objects} = Fetcher.fetch_collection(ap_id)
+ assert [%{"type" => "Create"}] = objects
+ end
+
+ test "it should stop fetching when we hit a 404" do
+ clear_config([:activitypub, :max_collection_objects], 1)
+
+ unordered_collection =
+ "test/fixtures/collections/unordered_page_reference.json"
+ |> File.read!()
+
+ first_page =
+ "test/fixtures/collections/unordered_page_first.json"
+ |> File.read!()
+
+ ap_id = "https://example.com/collection/unordered_page_reference"
+ first_page_id = "https://example.com/collection/unordered_page_reference?page=1"
+ second_page_id = "https://example.com/collection/unordered_page_reference?page=2"
+
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: ^ap_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: unordered_collection,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+
+ %{
+ method: :get,
+ url: ^first_page_id
+ } ->
+ %Tesla.Env{
+ status: 200,
+ body: first_page,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+
+ %{
+ method: :get,
+ url: ^second_page_id
+ } ->
+ %Tesla.Env{
+ status: 404,
+ body: nil,
+ headers: [{"content-type", "application/activity+json"}]
+ }
+ end)
+
+ {:ok, objects} = Fetcher.fetch_collection(ap_id)
assert [%{"type" => "Create"}] = objects
end
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index da5c87bd8..e209bb46b 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -782,6 +782,7 @@ test "mastodon pin/unpin", %{conn: conn} do
|> String.replace("{{status_id}}", status_id)
status_url = "https://example.com/users/lain/statuses/#{status_id}"
+ replies_url = status_url <> "/replies?only_other_accounts=true&page=true"
user =
File.read!("test/fixtures/users_mock/user.json")
@@ -820,6 +821,16 @@ test "mastodon pin/unpin", %{conn: conn} do
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
+
+ %{
+ method: :get,
+ url: ^replies_url
+ } ->
+ %Tesla.Env{
+ status: 404,
+ body: "",
+ headers: [{"content-type", "application/activity+json"}]
+ }
end)
data = %{
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index 1d73d6765..7c8e5a4e1 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -146,4 +146,17 @@ test "a misskey MFM status with a _misskey_content field should work and be link
"@akkoma_user "
end
end
+
+ test "a Note without replies/first/items validates" do
+ insert(:user, ap_id: "https://mastodon.social/users/emelie")
+
+ note =
+ "test/fixtures/tesla_mock/status.emelie.json"
+ |> File.read!()
+ |> Jason.decode!()
+ |> pop_in(["replies", "first", "items"])
+ |> elem(1)
+
+ %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
+ end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index 24df5ea61..002042802 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -380,7 +380,6 @@ test "schedules background fetching of `replies` items if max thread depth limit
clear_config([:instance, :federation_incoming_replies_max_depth], 10)
{:ok, activity} = Transmogrifier.handle_incoming(data)
-
object = Object.normalize(activity.data["object"])
assert object.data["replies"] == items
diff --git a/test/pleroma/web/plugs/http_signature_plug_test.exs b/test/pleroma/web/plugs/http_signature_plug_test.exs
index 02e8b3092..8ce956510 100644
--- a/test/pleroma/web/plugs/http_signature_plug_test.exs
+++ b/test/pleroma/web/plugs/http_signature_plug_test.exs
@@ -86,10 +86,12 @@ test "halts the connection when `signature` header is not present", %{conn: conn
test "aliases redirected /object endpoints", _ do
obj = insert(:note)
act = insert(:note_activity, note: obj)
- params = %{"actor" => "http://mastodon.example.org/users/admin"}
+ params = %{"actor" => "someparam"}
path = URI.parse(obj.data["id"]).path
conn = build_conn(:get, path, params)
- assert ["/notice/#{act.id}"] == HTTPSignaturePlug.route_aliases(conn)
+
+ assert ["/notice/#{act.id}", "/notice/#{act.id}?actor=someparam"] ==
+ HTTPSignaturePlug.route_aliases(conn)
end
end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 476e0ce04..ab44c489b 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -407,6 +407,15 @@ def get("http://mastodon.example.org/users/admin", _, _, _) do
}}
end
+ def get(
+ "http://mastodon.example.org/users/admin/statuses/99512778738411822/replies?min_id=99512778738411824&page=true",
+ _,
+ _,
+ _
+ ) do
+ {:ok, %Tesla.Env{status: 404, body: ""}}
+ end
+
def get("http://mastodon.example.org/users/relay", _, _, [
{"accept", "application/activity+json"}
]) do
From 95e4018c1a17bd96331cdeb19d1c62a599061351 Mon Sep 17 00:00:00 2001
From: Tusooa Zhu
Date: Fri, 19 Aug 2022 13:19:38 -0400
Subject: [PATCH 26/44] Disconnect streaming sessions when token is revoked
Use Websockex to replace websocket_client
Test that server will disconnect websocket upon token revocation
Lint
Execute session disconnect in background
Refactor streamer test
allow multi-streams
rebase websocket change
---
lib/pleroma/application.ex | 3 +-
.../web/mastodon_api/websocket_handler.ex | 6 +-
.../web/o_auth/token/strategy/revoke.ex | 14 ++-
lib/pleroma/web/streamer.ex | 24 ++++-
mix.exs | 2 +-
mix.lock | 2 +-
.../integration/mastodon_websocket_test.exs | 37 +++++--
test/pleroma/web/streamer_test.exs | 101 ++++++++++++++++++
test/support/websocket_client.ex | 34 +++---
9 files changed, 192 insertions(+), 31 deletions(-)
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index cb619232f..e11e5495a 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -63,7 +63,8 @@ def start(_type, _args) do
Pleroma.Repo,
Config.TransferTask,
Pleroma.Emoji,
- Pleroma.Web.Plugs.RateLimiter.Supervisor
+ Pleroma.Web.Plugs.RateLimiter.Supervisor,
+ {Task.Supervisor, name: Pleroma.TaskSupervisor}
] ++
cachex_children() ++
http_children() ++
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index 582e65d70..bd7c56243 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -59,7 +59,7 @@ def websocket_init(state) do
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}"
)
- Streamer.add_socket(state.topic, state.user)
+ Streamer.add_socket(state.topic, state.oauth_token)
{:ok, %{state | timer: timer()}}
end
@@ -139,6 +139,10 @@ def websocket_info(:tick, state) do
{:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
end
+ def websocket_info(:close, state) do
+ {:stop, state}
+ end
+
# State can be `[]` only in case we terminate before switching to websocket,
# we already log errors for these cases in `init/1`, so just do nothing here
def terminate(_reason, _req, []), do: :ok
diff --git a/lib/pleroma/web/o_auth/token/strategy/revoke.ex b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
index 8d6572704..de99bc137 100644
--- a/lib/pleroma/web/o_auth/token/strategy/revoke.ex
+++ b/lib/pleroma/web/o_auth/token/strategy/revoke.ex
@@ -21,6 +21,18 @@ def revoke(%App{} = app, %{"token" => token} = _attrs) do
@doc "Revokes access token"
@spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
def revoke(%Token{} = token) do
- Repo.delete(token)
+ with {:ok, token} <- Repo.delete(token) do
+ Task.Supervisor.start_child(
+ Pleroma.TaskSupervisor,
+ Pleroma.Web.Streamer,
+ :close_streams_by_oauth_token,
+ [token],
+ restart: :transient
+ )
+
+ {:ok, token}
+ else
+ result -> result
+ end
end
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index d5b1d0678..fba5d1c02 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -36,7 +36,7 @@ def registry, do: @registry
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
with {:ok, topic} <- get_topic(stream, user, oauth_token, params) do
- add_socket(topic, user)
+ add_socket(topic, oauth_token)
end
end
@@ -124,10 +124,10 @@ def get_topic(_stream, _user, _oauth_token, _params) do
end
@doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic."
- def add_socket(topic, user) do
+ def add_socket(topic, oauth_token) do
if should_env_send?() do
- auth? = if user, do: true
- Registry.register(@registry, topic, auth?)
+ oauth_token_id = if oauth_token, do: oauth_token.id, else: false
+ Registry.register(@registry, topic, oauth_token_id)
end
{:ok, topic}
@@ -311,6 +311,22 @@ defp thread_containment(activity, user) do
end
end
+ def close_streams_by_oauth_token(oauth_token) do
+ if should_env_send?() do
+ Registry.select(
+ @registry,
+ [
+ {
+ {:"$1", :"$2", :"$3"},
+ [{:==, :"$3", oauth_token.id}],
+ [:"$2"]
+ }
+ ]
+ )
+ |> Enum.each(fn pid -> send(pid, :close) end)
+ end
+ end
+
# In test environement, only return true if the registry is started.
# In benchmark environment, returns false.
# In any other environment, always returns true.
diff --git a/mix.exs b/mix.exs
index 768c8cbd6..ef038ce74 100644
--- a/mix.exs
+++ b/mix.exs
@@ -206,7 +206,7 @@ defp deps do
# temporary downgrade for excoveralls, hackney until hackney max_connections bug will be fixed
{:excoveralls, "0.12.3", only: :test},
{:mox, "~> 1.0", only: :test},
- {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
+ {:websockex, "~> 0.4.3", only: :test}
] ++ oauth_deps()
end
diff --git a/mix.lock b/mix.lock
index bda77cf5a..7eeb5c138 100644
--- a/mix.lock
+++ b/mix.lock
@@ -120,5 +120,5 @@
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
"vex": {:hex, :vex, "0.9.0", "613ea5eb3055662e7178b83e25b2df0975f68c3d8bb67c1645f0573e1a78d606", [:mix], [], "hexpm", "c69fff44d5c8aa3f1faee71bba1dcab05dd36364c5a629df8bb11751240c857f"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
- "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
+ "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
}
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
index 356bfa48d..9e266868d 100644
--- a/test/pleroma/integration/mastodon_websocket_test.exs
+++ b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -34,15 +34,20 @@ def start_socket(qs \\ nil, headers \\ []) do
test "allows multi-streams" do
capture_log(fn ->
assert {:ok, _} = start_socket()
- assert {:error, {404, _}} = start_socket("?stream=ncjdk")
+
+ assert {:error, %WebSockex.RequestError{code: 404, message: "Not Found"}} =
+ start_socket("?stream=ncjdk")
+
Process.sleep(30)
end)
end
test "requires authentication and a valid token for protected streams" do
capture_log(fn ->
- assert {:error, {401, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa")
- assert {:error, {401, _}} = start_socket("?stream=user")
+ assert {:error, %WebSockex.RequestError{code: 401}} =
+ start_socket("?stream=user&access_token=aaaaaaaaaaaa")
+
+ assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user")
Process.sleep(30)
end)
end
@@ -91,7 +96,7 @@ test "receives well formatted events" do
{:ok, token} = OAuth.Token.exchange_token(app, auth)
- %{user: user, token: token}
+ %{app: app, user: user, token: token}
end
test "accepts valid tokens", state do
@@ -102,7 +107,7 @@ test "accepts the 'user' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
capture_log(fn ->
- assert {:error, {401, _}} = start_socket("?stream=user")
+ assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user")
Process.sleep(30)
end)
end
@@ -111,7 +116,9 @@ test "accepts the 'user:notification' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
capture_log(fn ->
- assert {:error, {401, _}} = start_socket("?stream=user:notification")
+ assert {:error, %WebSockex.RequestError{code: 401}} =
+ start_socket("?stream=user:notification")
+
Process.sleep(30)
end)
end
@@ -120,11 +127,27 @@ test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
capture_log(fn ->
- assert {:error, {401, _}} =
+ assert {:error, %WebSockex.RequestError{code: 401}} =
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
Process.sleep(30)
end)
end
+
+ test "disconnect when token is revoked", %{app: app, user: user, token: token} do
+ assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
+ assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
+
+ {:ok, auth} = OAuth.Authorization.create_authorization(app, user)
+
+ {:ok, token2} = OAuth.Token.exchange_token(app, auth)
+ assert {:ok, _} = start_socket("?stream=user&access_token=#{token2.token}")
+
+ OAuth.Token.Strategy.Revoke.revoke(token)
+
+ assert_receive {:close, _}
+ assert_receive {:close, _}
+ refute_receive {:close, _}
+ end
end
end
diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs
index 07129ff11..9ae733fc6 100644
--- a/test/pleroma/web/streamer_test.exs
+++ b/test/pleroma/web/streamer_test.exs
@@ -760,4 +760,105 @@ test "it sends conversation update to the 'direct' stream when a message is dele
assert last_status["id"] == to_string(create_activity.id)
end
end
+
+ describe "stop streaming if token got revoked" do
+ setup do
+ child_proc = fn start, finalize ->
+ fn ->
+ start.()
+
+ receive do
+ {StreamerTest, :ready} ->
+ assert_receive {:render_with_user, _, "update.json", _, _}
+
+ receive do
+ {StreamerTest, :revoked} -> finalize.()
+ end
+ end
+ end
+ end
+
+ starter = fn user, token ->
+ fn -> Streamer.get_topic_and_add_socket("user", user, token) end
+ end
+
+ hit = fn -> assert_receive :close end
+ miss = fn -> refute_receive :close end
+
+ send_all = fn tasks, thing -> Enum.each(tasks, &send(&1.pid, thing)) end
+
+ %{
+ child_proc: child_proc,
+ starter: starter,
+ hit: hit,
+ miss: miss,
+ send_all: send_all
+ }
+ end
+
+ test "do not revoke other tokens", %{
+ child_proc: child_proc,
+ starter: starter,
+ hit: hit,
+ miss: miss,
+ send_all: send_all
+ } do
+ %{user: user, token: token} = oauth_access(["read"])
+ %{token: token2} = oauth_access(["read"], user: user)
+ %{user: user2, token: user2_token} = oauth_access(["read"])
+
+ post_user = insert(:user)
+ CommonAPI.follow(user, post_user)
+ CommonAPI.follow(user2, post_user)
+
+ tasks = [
+ Task.async(child_proc.(starter.(user, token), hit)),
+ Task.async(child_proc.(starter.(user, token2), miss)),
+ Task.async(child_proc.(starter.(user2, user2_token), miss))
+ ]
+
+ {:ok, _} =
+ CommonAPI.post(post_user, %{
+ status: "hi"
+ })
+
+ send_all.(tasks, {StreamerTest, :ready})
+
+ Pleroma.Web.OAuth.Token.Strategy.Revoke.revoke(token)
+
+ send_all.(tasks, {StreamerTest, :revoked})
+
+ Enum.each(tasks, &Task.await/1)
+ end
+
+ test "revoke all streams for this token", %{
+ child_proc: child_proc,
+ starter: starter,
+ hit: hit,
+ send_all: send_all
+ } do
+ %{user: user, token: token} = oauth_access(["read"])
+
+ post_user = insert(:user)
+ CommonAPI.follow(user, post_user)
+
+ tasks = [
+ Task.async(child_proc.(starter.(user, token), hit)),
+ Task.async(child_proc.(starter.(user, token), hit))
+ ]
+
+ {:ok, _} =
+ CommonAPI.post(post_user, %{
+ status: "hi"
+ })
+
+ send_all.(tasks, {StreamerTest, :ready})
+
+ Pleroma.Web.OAuth.Token.Strategy.Revoke.revoke(token)
+
+ send_all.(tasks, {StreamerTest, :revoked})
+
+ Enum.each(tasks, &Task.await/1)
+ end
+ end
end
diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex
index 34b955474..70d331999 100644
--- a/test/support/websocket_client.ex
+++ b/test/support/websocket_client.ex
@@ -5,18 +5,17 @@
defmodule Pleroma.Integration.WebsocketClient do
# https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs
+ use WebSockex
+
@doc """
Starts the WebSocket server for given ws URL. Received Socket.Message's
are forwarded to the sender pid
"""
def start_link(sender, url, headers \\ []) do
- :crypto.start()
- :ssl.start()
-
- :websocket_client.start_link(
- String.to_charlist(url),
+ WebSockex.start_link(
+ url,
__MODULE__,
- [sender],
+ %{sender: sender},
extra_headers: headers
)
end
@@ -36,27 +35,32 @@ def send_text(server_pid, msg) do
end
@doc false
- def init([sender], _conn_state) do
- {:ok, %{sender: sender}}
- end
-
- @doc false
- def websocket_handle(frame, _conn_state, state) do
+ @impl true
+ def handle_frame(frame, state) do
send(state.sender, frame)
{:ok, state}
end
+ @impl true
+ def handle_disconnect(conn_status, state) do
+ send(state.sender, {:close, conn_status})
+ {:ok, state}
+ end
+
@doc false
- def websocket_info({:text, msg}, _conn_state, state) do
+ @impl true
+ def handle_info({:text, msg}, state) do
{:reply, {:text, msg}, state}
end
- def websocket_info(:close, _conn_state, _state) do
+ @impl true
+ def handle_info(:close, _state) do
{:close, <<>>, "done"}
end
@doc false
- def websocket_terminate(_reason, _conn_state, _state) do
+ @impl true
+ def terminate(_reason, _state) do
:ok
end
end
From 722e56b3086318d01210884b8aa0ae7945833bc6 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Sat, 27 Aug 2022 19:12:15 +0100
Subject: [PATCH 27/44] add changelog entry
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 183a60d10..1a71255ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Resolution of nested mix tasks (i.e search.meilisearch) in OTP releases
- Elasticsearch returning likes and repeats, displaying as posts
- Ensure key generation happens at registration-time to prevent potential race-conditions
+- Ensured websockets get closed on logout
+- Allowed GoToSocial-style `?query_string` signatures
### Removed
- Non-finch HTTP adapters. `:tesla, :adapter` is now highly recommended to be set to the default.
From df39cab9c13e470e2b2d62550079ae5df0c18fec Mon Sep 17 00:00:00 2001
From: floatingghost
Date: Mon, 29 Aug 2022 19:42:22 +0000
Subject: [PATCH 28/44] Automatic status translation (#187)
Fixes #115
Co-authored-by: FloatingGhost
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/187
---
CHANGELOG.md | 1 +
config/config.exs | 13 +++
config/description.exs | 73 ++++++++++++++-
docs/docs/configuration/cheatsheet.md | 25 +++++
lib/pleroma/akkoma/translators/deepl.ex | 58 ++++++++++++
.../akkoma/translators/libre_translate.ex | 51 +++++++++++
lib/pleroma/akkoma/translators/translator.ex | 3 +
lib/pleroma/application.ex | 3 +-
.../api_spec/operations/status_operation.ex | 36 ++++++++
.../controllers/status_controller.ex | 45 ++++++++-
.../web/mastodon_api/views/instance_view.ex | 3 +
lib/pleroma/web/router.ex | 1 +
test/pleroma/translators/deepl_test.exs | 75 +++++++++++++++
.../translators/libre_translate_test.exs | 91 +++++++++++++++++++
.../controllers/status_controller_test.exs | 72 +++++++++++++++
15 files changed, 543 insertions(+), 7 deletions(-)
create mode 100644 lib/pleroma/akkoma/translators/deepl.ex
create mode 100644 lib/pleroma/akkoma/translators/libre_translate.ex
create mode 100644 lib/pleroma/akkoma/translators/translator.ex
create mode 100644 test/pleroma/translators/deepl_test.exs
create mode 100644 test/pleroma/translators/libre_translate_test.exs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a71255ff..05cb69c40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- support for setting instance languages in metadata
- support for reusing oauth tokens, and not requiring new authorizations
- the ability to obfuscate domains in your MRF descriptions
+- automatic translation of statuses via DeepL or LibreTranslate
### Changed
- MFM parsing is now done on the backend by a modified version of ilja's parser -> https://akkoma.dev/AkkomaGang/mfm-parser
diff --git a/config/config.exs b/config/config.exs
index 5ae7a33a2..330e572fe 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -843,6 +843,19 @@
}
}
+config :pleroma, :translator,
+ enabled: false,
+ module: Akkoma.Translators.DeepL
+
+config :pleroma, :deepl,
+ # either :free or :pro
+ tier: :free,
+ api_key: ""
+
+config :pleroma, :libre_translate,
+ url: "http://127.0.0.1:5000",
+ api_key: nil
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
index 61ef8f449..a17897b98 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -3226,13 +3226,14 @@
group: :pleroma,
key: Pleroma.Search,
type: :group,
+ label: "Search",
description: "General search settings.",
children: [
%{
key: :module,
- type: :keyword,
+ type: :module,
description: "Selected search module.",
- suggestion: [Pleroma.Search.DatabaseSearch, Pleroma.Search.Meilisearch]
+ suggestions: {:list_behaviour_implementations, Pleroma.Search.SearchBackend}
}
]
},
@@ -3257,7 +3258,7 @@
},
%{
key: :initial_indexing_chunk_size,
- type: :int,
+ type: :integer,
description:
"Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
" since there's a limit on maximum insert size",
@@ -3268,6 +3269,7 @@
%{
group: :pleroma,
key: Pleroma.Search.Elasticsearch.Cluster,
+ label: "Elasticsearch",
type: :group,
description: "Elasticsearch settings.",
children: [
@@ -3334,13 +3336,13 @@
},
%{
key: :bulk_page_size,
- type: :int,
+ type: :integer,
description: "Size for bulk put requests, mostly used on building the index",
suggestion: [5000]
},
%{
key: :bulk_wait_interval,
- type: :int,
+ type: :integer,
description: "Time to wait between bulk put requests (in ms)",
suggestion: [15_000]
}
@@ -3349,5 +3351,66 @@
]
}
]
+ },
+ %{
+ group: :pleroma,
+ key: :translator,
+ type: :group,
+ description: "Translation Settings",
+ children: [
+ %{
+ key: :enabled,
+ type: :boolean,
+ description: "Is translation enabled?",
+ suggestion: [true, false]
+ },
+ %{
+ key: :module,
+ type: :module,
+ description: "Translation module.",
+ suggestions: {:list_behaviour_implementations, Pleroma.Akkoma.Translator}
+ }
+ ]
+ },
+ %{
+ group: :pleroma,
+ key: :deepl,
+ label: "DeepL",
+ type: :group,
+ description: "DeepL Settings.",
+ children: [
+ %{
+ key: :tier,
+ type: {:dropdown, :atom},
+ description: "API Tier",
+ suggestions: [:free, :pro]
+ },
+ %{
+ key: :api_key,
+ type: :string,
+ description: "API key for DeepL",
+ suggestions: [nil]
+ }
+ ]
+ },
+ %{
+ group: :pleroma,
+ key: :libre_translate,
+ type: :group,
+ description: "LibreTranslate Settings.",
+ children: [
+ %{
+ key: :url,
+ type: :string,
+ description: "URL for libretranslate",
+ suggestion: [nil]
+ },
+ %{
+ key: :api_key,
+ type: :string,
+ description: "API key for libretranslate",
+ suggestion: [nil]
+ }
+ ]
}
]
diff --git a/docs/docs/configuration/cheatsheet.md b/docs/docs/configuration/cheatsheet.md
index a29db208c..90041d3d6 100644
--- a/docs/docs/configuration/cheatsheet.md
+++ b/docs/docs/configuration/cheatsheet.md
@@ -1159,3 +1159,28 @@ Each job has these settings:
* `:max_running` - max concurrently runnings jobs
* `:max_waiting` - max waiting jobs
+
+### Translation Settings
+
+Settings to automatically translate statuses for end users. Currently supported
+translation services are DeepL and LibreTranslate.
+
+Translations are available at `/api/v1/statuses/:id/translations/:language`, where
+`language` is the target language code (e.g `en`)
+
+### `:translator`
+
+- `:enabled` - enables translation
+- `:module` - Sets module to be used
+ - Either `Pleroma.Akkoma.Translators.DeepL` or `Pleroma.Akkoma.Translators.LibreTranslate`
+
+### `:deepl`
+
+- `:api_key` - API key for DeepL
+- `:tier` - API tier
+ - either `:free` or `:pro`
+
+### `:libre_translate`
+
+- `:url` - URL of LibreTranslate instance
+- `:api_key` - API key for LibreTranslate
\ No newline at end of file
diff --git a/lib/pleroma/akkoma/translators/deepl.ex b/lib/pleroma/akkoma/translators/deepl.ex
new file mode 100644
index 000000000..0a4a7fe10
--- /dev/null
+++ b/lib/pleroma/akkoma/translators/deepl.ex
@@ -0,0 +1,58 @@
+defmodule Pleroma.Akkoma.Translators.DeepL do
+ @behaviour Pleroma.Akkoma.Translator
+
+ alias Pleroma.HTTP
+ alias Pleroma.Config
+ require Logger
+
+ defp base_url(:free) do
+ "https://api-free.deepl.com/v2/"
+ end
+
+ defp base_url(:pro) do
+ "https://api.deepl.com/v2/"
+ end
+
+ defp api_key do
+ Config.get([:deepl, :api_key])
+ end
+
+ defp tier do
+ Config.get([:deepl, :tier])
+ end
+
+ @impl Pleroma.Akkoma.Translator
+ def translate(string, to_language) do
+ with {:ok, %{status: 200} = response} <- do_request(api_key(), tier(), string, to_language),
+ {:ok, body} <- Jason.decode(response.body) do
+ %{"translations" => [%{"text" => translated, "detected_source_language" => detected}]} =
+ body
+
+ {:ok, detected, translated}
+ else
+ {:ok, %{status: status} = response} ->
+ Logger.warning("DeepL: Request rejected: #{inspect(response)}")
+ {:error, "DeepL request failed (code #{status})"}
+
+ {:error, reason} ->
+ {:error, reason}
+ end
+ end
+
+ defp do_request(api_key, tier, string, to_language) do
+ HTTP.post(
+ base_url(tier) <> "translate",
+ URI.encode_query(
+ %{
+ text: string,
+ target_lang: to_language
+ },
+ :rfc3986
+ ),
+ [
+ {"authorization", "DeepL-Auth-Key #{api_key}"},
+ {"content-type", "application/x-www-form-urlencoded"}
+ ]
+ )
+ end
+end
diff --git a/lib/pleroma/akkoma/translators/libre_translate.ex b/lib/pleroma/akkoma/translators/libre_translate.ex
new file mode 100644
index 000000000..615d04192
--- /dev/null
+++ b/lib/pleroma/akkoma/translators/libre_translate.ex
@@ -0,0 +1,51 @@
+defmodule Pleroma.Akkoma.Translators.LibreTranslate do
+ @behaviour Pleroma.Akkoma.Translator
+
+ alias Pleroma.Config
+ alias Pleroma.HTTP
+ require Logger
+
+ defp api_key do
+ Config.get([:libre_translate, :api_key])
+ end
+
+ defp url do
+ Config.get([:libre_translate, :url])
+ end
+
+ @impl Pleroma.Akkoma.Translator
+ def translate(string, to_language) do
+ with {:ok, %{status: 200} = response} <- do_request(string, to_language),
+ {:ok, body} <- Jason.decode(response.body) do
+ %{"translatedText" => translated, "detectedLanguage" => %{"language" => detected}} = body
+
+ {:ok, detected, translated}
+ else
+ {:ok, %{status: status} = response} ->
+ Logger.warning("libre_translate: request failed, #{inspect(response)}")
+ {:error, "libre_translate: request failed (code #{status})"}
+
+ {:error, reason} ->
+ {:error, reason}
+ end
+ end
+
+ defp do_request(string, to_language) do
+ url = URI.parse(url())
+ url = %{url | path: "/translate"}
+
+ HTTP.post(
+ to_string(url),
+ Jason.encode!(%{
+ q: string,
+ source: "auto",
+ target: to_language,
+ format: "html",
+ api_key: api_key()
+ }),
+ [
+ {"content-type", "application/json"}
+ ]
+ )
+ end
+end
diff --git a/lib/pleroma/akkoma/translators/translator.ex b/lib/pleroma/akkoma/translators/translator.ex
new file mode 100644
index 000000000..0276ed6c2
--- /dev/null
+++ b/lib/pleroma/akkoma/translators/translator.ex
@@ -0,0 +1,3 @@
+defmodule Pleroma.Akkoma.Translator do
+ @callback translate(String.t(), String.t()) :: {:ok, String.t(), String.t()} | {:error, any()}
+end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index e11e5495a..b809f7733 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -154,7 +154,8 @@ defp cachex_children do
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
- build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000)
+ build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
+ build_cachex("translations", default_ttl: :timer.hours(24 * 30), limit: 2500)
]
end
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
index a5da8b58e..04a7bf5db 100644
--- a/lib/pleroma/web/api_spec/operations/status_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -406,6 +406,22 @@ def bookmarks_operation do
}
end
+ def translate_operation do
+ %Operation{
+ tags: ["Retrieve status translation"],
+ summary: "Translate status",
+ description: "View the translation of a given status",
+ operationId: "StatusController.translation",
+ security: [%{"oAuth" => ["read:statuses"]}],
+ parameters: [id_param(), language_param()],
+ responses: %{
+ 200 => Operation.response("Translation", "application/json", translation()),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
def array_of_statuses do
%Schema{type: :array, items: Status, example: [Status.schema().example]}
end
@@ -552,6 +568,10 @@ def id_param do
)
end
+ defp language_param do
+ Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en")
+ end
+
defp status_response do
Operation.response("Status", "application/json", Status)
end
@@ -573,4 +593,20 @@ defp context do
}
}
end
+
+ defp translation do
+ %Schema{
+ title: "StatusTranslation",
+ description: "The translation of a status.",
+ type: :object,
+ required: [:detected_language, :text],
+ properties: %{
+ detected_language: %Schema{
+ type: :string,
+ description: "The detected language of the text"
+ },
+ text: %Schema{type: :string, description: "The translated text"}
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 9ab30742b..d9b93ca5e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Bookmark
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Config
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -30,6 +31,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug(:skip_public_check when action in [:index, :show])
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
plug(
OAuthScopesPlug,
@@ -37,7 +39,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
when action in [
:index,
:show,
- :context
+ :context,
+ :translate
]
)
@@ -418,6 +421,46 @@ def bookmarks(%{assigns: %{user: user}} = conn, params) do
)
end
+ @doc "GET /api/v1/statuses/:id/translations/:language"
+ def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language}) do
+ with {:enabled, true} <- {:enabled, Config.get([:translator, :enabled])},
+ %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
+ translation_module <- Config.get([:translator, :module]),
+ {:ok, detected, translation} <-
+ fetch_or_translate(
+ activity.id,
+ activity.object.data["content"],
+ language,
+ translation_module
+ ) do
+ json(conn, %{detected_language: detected, text: translation})
+ else
+ {:enabled, false} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => "Translation is not enabled"})
+
+ {:visible, false} ->
+ {:error, :not_found}
+
+ e ->
+ e
+ end
+ end
+
+ defp fetch_or_translate(status_id, text, language, translation_module) do
+ @cachex.fetch!(:user_cache, "translations:#{status_id}:#{language}", fn _ ->
+ value = translation_module.translate(text, language)
+
+ with {:ok, _, _} <- value do
+ value
+ else
+ _ -> {:ignore, value}
+ end
+ end)
+ end
+
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 7ae357e23..436519439 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -81,6 +81,9 @@ def features do
if Config.get([:instance, :profile_directory]) do
"profile_directory"
end,
+ if Config.get([:translator, :enabled], false) do
+ "akkoma:machine_translation"
+ end,
"custom_emoji_reactions"
]
|> Enum.filter(& &1)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 647d99278..aff7b67db 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -553,6 +553,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
+ get("/statuses/:id/translations/:language", StatusController, :translate)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
diff --git a/test/pleroma/translators/deepl_test.exs b/test/pleroma/translators/deepl_test.exs
new file mode 100644
index 000000000..286d21d3e
--- /dev/null
+++ b/test/pleroma/translators/deepl_test.exs
@@ -0,0 +1,75 @@
+defmodule Pleroma.Akkoma.Translators.DeepLTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Akkoma.Translators.DeepL
+
+ describe "translating with deepl" do
+ setup do
+ clear_config([:deepl, :api_key], "deepl_api_key")
+ end
+
+ test "should work with the free tier" do
+ clear_config([:deepl, :tier], :free)
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "https://api-free.deepl.com/v2/translate"} = env ->
+ auth_header = Enum.find(env.headers, fn {k, _v} -> k == "authorization" end)
+ assert {"authorization", "DeepL-Auth-Key deepl_api_key"} = auth_header
+
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ translations: [
+ %{
+ "text" => "I will crush you",
+ "detected_source_language" => "ja"
+ }
+ ]
+ })
+ }
+ end)
+
+ assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+
+ test "should work with the pro tier" do
+ clear_config([:deepl, :tier], :pro)
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "https://api.deepl.com/v2/translate"} = env ->
+ auth_header = Enum.find(env.headers, fn {k, _v} -> k == "authorization" end)
+ assert {"authorization", "DeepL-Auth-Key deepl_api_key"} = auth_header
+
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ translations: [
+ %{
+ "text" => "I will crush you",
+ "detected_source_language" => "ja"
+ }
+ ]
+ })
+ }
+ end)
+
+ assert {:ok, "ja", "I will crush you"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+
+ test "should gracefully fail if the API errors" do
+ clear_config([:deepl, :tier], :free)
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
+ %Tesla.Env{
+ status: 403,
+ body: ""
+ }
+ end)
+
+ assert {:error, "DeepL request failed (code 403)"} = DeepL.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+ end
+end
diff --git a/test/pleroma/translators/libre_translate_test.exs b/test/pleroma/translators/libre_translate_test.exs
new file mode 100644
index 000000000..9ed2c5323
--- /dev/null
+++ b/test/pleroma/translators/libre_translate_test.exs
@@ -0,0 +1,91 @@
+defmodule Pleroma.Akkoma.Translators.LibreTranslateTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Akkoma.Translators.LibreTranslate
+
+ describe "translating with libre translate" do
+ setup do
+ clear_config([:libre_translate, :url], "http://libre.translate/translate")
+ end
+
+ test "should work without an API key" do
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "http://libre.translate/translate"} = env ->
+ assert {:ok, %{"api_key" => nil}} = Jason.decode(env.body)
+
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ detectedLanguage: %{
+ confidence: 83,
+ language: "ja"
+ },
+ translatedText: "I will crush you"
+ })
+ }
+ end)
+
+ assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+
+ test "should work with an API key" do
+ clear_config([:libre_translate, :api_key], "libre_translate_api_key")
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "http://libre.translate/translate"} = env ->
+ assert {:ok, %{"api_key" => "libre_translate_api_key"}} = Jason.decode(env.body)
+
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ detectedLanguage: %{
+ confidence: 83,
+ language: "ja"
+ },
+ translatedText: "I will crush you"
+ })
+ }
+ end)
+
+ assert {:ok, "ja", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+
+ test "should gracefully handle API key errors" do
+ clear_config([:libre_translate, :api_key], "")
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "http://libre.translate/translate"} ->
+ %Tesla.Env{
+ status: 403,
+ body:
+ Jason.encode!(%{
+ error: "Please contact the server operator to obtain an API key"
+ })
+ }
+ end)
+
+ assert {:error, "libre_translate: request failed (code 403)"} =
+ LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "en")
+ end
+
+ test "should gracefully handle an unsupported language" do
+ clear_config([:libre_translate, :api_key], "")
+
+ Tesla.Mock.mock(fn
+ %{method: :post, url: "http://libre.translate/translate"} ->
+ %Tesla.Env{
+ status: 400,
+ body:
+ Jason.encode!(%{
+ error: "zoop is not supported"
+ })
+ }
+ end)
+
+ assert {:error, "libre_translate: request failed (code 400)"} =
+ LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "zoop")
+ end
+ end
+end
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index ea168f6c5..e38f5fe58 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -2071,4 +2071,76 @@ test "posting a quote of a status that doesn't exist", %{conn: conn} do
|> json_response_and_validate_schema(422)
end
end
+
+ describe "translating statuses" do
+ setup do
+ clear_config([:translator, :enabled], true)
+ clear_config([:translator, :module], Pleroma.Akkoma.Translators.DeepL)
+ clear_config([:deepl, :api_key], "deepl_api_key")
+ oauth_access(["read:statuses"])
+ end
+
+ test "should return text and detected language", %{conn: conn} do
+ clear_config([:deepl, :tier], :free)
+
+ Tesla.Mock.mock_global(fn
+ %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ translations: [
+ %{
+ "text" => "Tell me, for whom do you fight?",
+ "detected_source_language" => "ja"
+ }
+ ]
+ })
+ }
+ end)
+
+ user = insert(:user)
+ {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?"})
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
+
+ response = json_response_and_validate_schema(conn, 200)
+
+ assert response["text"] == "Tell me, for whom do you fight?"
+ assert response["detected_language"] == "ja"
+ end
+
+ test "should not allow translating of statuses you cannot see", %{conn: conn} do
+ clear_config([:deepl, :tier], :free)
+
+ Tesla.Mock.mock_global(fn
+ %{method: :post, url: "https://api-free.deepl.com/v2/translate"} ->
+ %Tesla.Env{
+ status: 200,
+ body:
+ Jason.encode!(%{
+ translations: [
+ %{
+ "text" => "Tell me, for whom do you fight?",
+ "detected_source_language" => "ja"
+ }
+ ]
+ })
+ }
+ end)
+
+ user = insert(:user)
+ {:ok, to_translate} = CommonAPI.post(user, %{status: "何のために闘う?", visibility: "private"})
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/statuses/#{to_translate.id}/translations/en")
+
+ json_response_and_validate_schema(conn, 404)
+ end
+ end
end
From 7759187de90b8894b8efa2ba111fc73ae48ed324 Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Mon, 29 Aug 2022 22:20:47 +0100
Subject: [PATCH 29/44] ensure default value is sane
---
config/config.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/config.exs b/config/config.exs
index 330e572fe..bf7e7db44 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -845,7 +845,7 @@
config :pleroma, :translator,
enabled: false,
- module: Akkoma.Translators.DeepL
+ module: Pleroma.Akkoma.Translators.DeepL
config :pleroma, :deepl,
# either :free or :pro
From 9cb41b6d7bb6788c0416952e64a582bc06f6d2dd Mon Sep 17 00:00:00 2001
From: FloatingGhost
Date: Tue, 30 Aug 2022 10:39:36 +0100
Subject: [PATCH 30/44] add extra instructions to placeholder page
---
priv/static/index.html | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/priv/static/index.html b/priv/static/index.html
index 4a304f576..e60d31966 100644
--- a/priv/static/index.html
+++ b/priv/static/index.html
@@ -6,7 +6,18 @@
Welcome to Akkoma!
If you're seeing this page, your server works!
- In order to get a frontend to show here, you'll need to set up :pleroma, :frontends, primary
and install your frontend of choice
- Documentation
+ In order to get a frontend to show here, you'll need to set up :pleroma, :frontends, primary
and install your frontend of choice, in most cases this will just be:
+
+
+ # OTP
+ ./bin/pleroma_ctl frontend install pleroma-fe --ref stable
+ # Source
+ mix pleroma.frontend install pleroma-fe --ref stable
+
+ ## you can do the same thing for admin-fe if you so wish
+
+
+ Installation Command Documentation
+ Config Documentation