diff --git a/CHANGELOG.md b/CHANGELOG.md index 3966bb10b..05cb69c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ 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 +- 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 @@ -17,6 +21,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. diff --git a/config/config.exs b/config/config.exs index 83977da19..bf7e7db44 100644 --- a/config/config.exs +++ b/config/config.exs @@ -197,6 +197,7 @@ config :pleroma, :instance, 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, @@ -793,7 +794,8 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false 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 @@ -841,6 +843,19 @@ config :pleroma, Pleroma.Search.Elasticsearch.Cluster, } } +config :pleroma, :translator, + enabled: false, + module: Pleroma.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 b70982cd2..a17897b98 100644 --- a/config/description.exs +++ b/config/description.exs @@ -509,6 +509,16 @@ config :pleroma, :config_description, [ "Pleroma" ] }, + %{ + key: :languages, + type: {:list, :string}, + description: "Languages the instance uses", + suggestions: [ + "en", + "ja", + "fr" + ] + }, %{ key: :email, label: "Admin Email Address", @@ -3216,13 +3226,14 @@ config :pleroma, :config_description, [ 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} } ] }, @@ -3247,7 +3258,7 @@ config :pleroma, :config_description, [ }, %{ 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", @@ -3258,6 +3269,7 @@ config :pleroma, :config_description, [ %{ group: :pleroma, key: Pleroma.Search.Elasticsearch.Cluster, + label: "Elasticsearch", type: :group, description: "Elasticsearch settings.", children: [ @@ -3324,13 +3336,13 @@ config :pleroma, :config_description, [ }, %{ 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] } @@ -3339,5 +3351,66 @@ config :pleroma, :config_description, [ ] } ] + }, + %{ + 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 8fa188de1..52062eaa0 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 @@ -1158,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 diff --git a/docs/docs/installation/alpine_linux_en.md b/docs/docs/installation/alpine_linux_en.md index f98998fb8..aae8f9626 100644 --- a/docs/docs/installation/alpine_linux_en.md +++ b/docs/docs/installation/alpine_linux_en.md @@ -221,6 +221,8 @@ If your instance is up and running, you can create your first user with administ doas -u akkoma env MIX_ENV=prod mix pleroma.user new --admin ``` +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/arch_linux_en.md b/docs/docs/installation/arch_linux_en.md index f7a7d6239..639c9c798 100644 --- a/docs/docs/installation/arch_linux_en.md +++ b/docs/docs/installation/arch_linux_en.md @@ -212,6 +212,8 @@ If your instance is up and running, you can create your first user with administ sudo -Hu akkoma MIX_ENV=prod mix pleroma.user new --admin ``` +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/debian_based_en.md b/docs/docs/installation/debian_based_en.md index 40503db0c..139c789bc 100644 --- a/docs/docs/installation/debian_based_en.md +++ b/docs/docs/installation/debian_based_en.md @@ -175,6 +175,8 @@ If your instance is up and running, you can create your first user with administ sudo -Hu akkoma MIX_ENV=prod mix pleroma.user new --admin ``` +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/fedora_based_en.md b/docs/docs/installation/fedora_based_en.md index 30d68d97f..d8c7b3e74 100644 --- a/docs/docs/installation/fedora_based_en.md +++ b/docs/docs/installation/fedora_based_en.md @@ -199,6 +199,8 @@ If your instance is up and running, you can create your first user with administ sudo -Hu akkoma MIX_ENV=prod mix pleroma.user new --admin ``` +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/freebsd_en.md b/docs/docs/installation/freebsd_en.md index be735a998..53c029d27 100644 --- a/docs/docs/installation/freebsd_en.md +++ b/docs/docs/installation/freebsd_en.md @@ -206,6 +206,9 @@ If your instance is up and running, you can create your first user with administ ```shell sudo -Hu akkoma MIX_ENV=prod mix pleroma.user new --admin ``` + +{! installation/frontends.include !} + ## Conclusion Restart nginx with `# service nginx restart` and you should be up and running. diff --git a/docs/docs/installation/frontends.include b/docs/docs/installation/frontends.include new file mode 100644 index 000000000..585be71ae --- /dev/null +++ b/docs/docs/installation/frontends.include @@ -0,0 +1,25 @@ +#### Installing Frontends + +Once your backend server is functional, you'll also want to +probably install frontends. + +These are no longer bundled with the distribution and need an extra +command to install. + +For most installations, the following will suffice: + +=== "OTP" + ```sh + ./bin/pleroma_ctl frontend install pleroma-fe --ref stable + # and also, if desired + ./bin/pleroma_ctl frontend install admin-fe --ref stable + ``` + +=== "From Source" + ```sh + mix pleroma.frontend install pleroma-fe --ref stable + mix pleroma.frontend install admin-fe --ref stable + ``` + +For more customised installations, refer to [Frontend Management](../../configuration/frontend_management) + diff --git a/docs/docs/installation/gentoo_en.md b/docs/docs/installation/gentoo_en.md index 4649b63bf..9450c9b38 100644 --- a/docs/docs/installation/gentoo_en.md +++ b/docs/docs/installation/gentoo_en.md @@ -293,6 +293,8 @@ akkoma$ MIX_ENV=prod mix pleroma.user new --admin If you opted to allow sudo for the `akkoma` user but would like to remove the ability for greater security, now might be a good time to edit `/etc/sudoers` and/or change the groups the `akkoma` user belongs to. Be sure to restart the akkoma service afterwards to ensure it picks up on the changes. +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/migrating_to_akkoma.md b/docs/docs/installation/migrating_to_akkoma.md index 74b87e318..d8ea0ea25 100644 --- a/docs/docs/installation/migrating_to_akkoma.md +++ b/docs/docs/installation/migrating_to_akkoma.md @@ -1,7 +1,5 @@ # Migrating to Akkoma -**Akkoma does not currently have a stable release, until 3.0, all builds should be considered "develop"** - ## Why should you migrate? aside from actually responsive maintainer(s)? let's lookie here, we've got: @@ -11,6 +9,8 @@ aside from actually responsive maintainer(s)? let's lookie here, we've got: - elasticsearch support (because pleroma search is GARBAGE) - latest develop pleroma-fe additions - local-only posting +- automatic post translation +- the mastodon frontend back in all its glory - probably more, this is like 3.5 years of IHBA additions finally compiled ## Actually migrating @@ -43,14 +43,14 @@ This will just be setting the update URL - find your flavour from the [mapping o ```bash export FLAVOUR=[the flavour you found above] -./bin/pleroma_ctl update --zip-url https://akkoma-updates.s3-website.fr-par.scw.cloud/develop/akkoma-$FLAVOUR.zip +./bin/pleroma_ctl update --zip-url https://akkoma-updates.s3-website.fr-par.scw.cloud/stable/akkoma-$FLAVOUR.zip ./bin/pleroma_ctl migrate ``` Then restart. When updating in the future, you canjust use ```bash -./bin/pleroma_ctl update --branch develop +./bin/pleroma_ctl update --branch stable ``` ## Frontend changes @@ -62,17 +62,18 @@ your upgrade path here depends on your setup You'll need to run a couple of commands, -```bash -# From source -mix pleroma.frontend install pleroma-fe -# you'll probably want this too -mix pleroma.frontend install admin-fe +=== "OTP" + ```sh + ./bin/pleroma_ctl frontend install pleroma-fe --ref stable + # and also, if desired + ./bin/pleroma_ctl frontend install admin-fe --ref stable + ``` -# OTP -./bin/pleroma_ctl frontend install pleroma-fe -# you'll probably want this too -./bin/pleroma_ctl frontend install admin-fe -``` +=== "From Source" + ```sh + mix pleroma.frontend install pleroma-fe --ref stable + mix pleroma.frontend install admin-fe --ref stable + ``` ### I've run the mix task to install a frontend diff --git a/docs/docs/installation/netbsd_en.md b/docs/docs/installation/netbsd_en.md index c00a32e34..f13a3ee89 100644 --- a/docs/docs/installation/netbsd_en.md +++ b/docs/docs/installation/netbsd_en.md @@ -202,6 +202,8 @@ incorrect timestamps. You should have ntpd running. * +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/openbsd_en.md b/docs/docs/installation/openbsd_en.md index c7e8cf0c0..581942f99 100644 --- a/docs/docs/installation/openbsd_en.md +++ b/docs/docs/installation/openbsd_en.md @@ -250,6 +250,8 @@ If your instance is up and running, you can create your first user with administ LC_ALL=en_US.UTF-8 MIX_ENV=prod mix pleroma.user new --admin ``` +{! installation/frontends.include !} + #### Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/otp_en.md b/docs/docs/installation/otp_en.md index 022716fec..329afe967 100644 --- a/docs/docs/installation/otp_en.md +++ b/docs/docs/installation/otp_en.md @@ -306,6 +306,8 @@ su akkoma -s $SHELL -lc "./bin/pleroma_ctl user new joeuser joeuser@sld.tld --ad ``` This will create an account withe the username of 'joeuser' with the email address of joeuser@sld.tld, and set that user's account as an admin. This will result in a link that you can paste into the browser, which logs you in and enables you to set the password. +{! installation/frontends.include !} + ## Further reading {! installation/further_reading.include !} diff --git a/docs/docs/installation/otp_redhat_en.md b/docs/docs/installation/otp_redhat_en.md index 2e6b58c9e..ec6c30bcf 100644 --- a/docs/docs/installation/otp_redhat_en.md +++ b/docs/docs/installation/otp_redhat_en.md @@ -279,6 +279,7 @@ After that, run the `pleroma_ctl migrate` command as usual to perform database m As it currently stands, your OTP build will only be compatible for the specific RedHat distribution you've built it on. Fedora builds only work on Fedora, Centos builds only on Centos, RedHat builds only on RedHat. Secondly, for Fedora, they will also be bound to the specific Fedora release. This is because different releases of Fedora may have significant changes made in some of the required packages and libraries. +{! installation/frontends.include !} {! installation/further_reading.include !} diff --git a/lib/pleroma/akkoma/translators/deepl.ex b/lib/pleroma/akkoma/translators/deepl.ex new file mode 100644 index 000000000..da6b8a582 --- /dev/null +++ b/lib/pleroma/akkoma/translators/deepl.ex @@ -0,0 +1,100 @@ +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 languages do + with {:ok, %{status: 200} = source_response} <- do_languages("source"), + {:ok, %{status: 200} = dest_response} <- do_languages("target"), + {:ok, source_body} <- Jason.decode(source_response.body), + {:ok, dest_body} <- Jason.decode(dest_response.body) do + source_resp = + Enum.map(source_body, fn %{"language" => code, "name" => name} -> + %{code: code, name: name} + end) + + dest_resp = + Enum.map(dest_body, fn %{"language" => code, "name" => name} -> + %{code: code, name: name} + end) + + {:ok, source_resp, dest_resp} + else + {:ok, %{status: status} = response} -> + Logger.warning("DeepL: Request rejected: #{inspect(response)}") + {:error, "DeepL request failed (code #{status})"} + + {:error, reason} -> + {:error, reason} + end + end + + @impl Pleroma.Akkoma.Translator + def translate(string, from_language, to_language) do + with {:ok, %{status: 200} = response} <- + do_request(api_key(), tier(), string, from_language, 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, from_language, to_language) do + HTTP.post( + base_url(tier) <> "translate", + URI.encode_query( + %{ + text: string, + target_lang: to_language, + tag_handling: "html" + } + |> maybe_add_source(from_language), + :rfc3986 + ), + [ + {"authorization", "DeepL-Auth-Key #{api_key}"}, + {"content-type", "application/x-www-form-urlencoded"} + ] + ) + end + + defp maybe_add_source(opts, nil), do: opts + defp maybe_add_source(opts, lang), do: Map.put(opts, :source_lang, lang) + + defp do_languages(type) do + HTTP.get( + base_url(tier()) <> "languages?type=#{type}", + [ + {"authorization", "DeepL-Auth-Key #{api_key()}"} + ] + ) + 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..3a8d9d827 --- /dev/null +++ b/lib/pleroma/akkoma/translators/libre_translate.ex @@ -0,0 +1,82 @@ +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 languages do + with {:ok, %{status: 200} = response} <- do_languages(), + {:ok, body} <- Jason.decode(response.body) do + resp = Enum.map(body, fn %{"code" => code, "name" => name} -> %{code: code, name: name} end) + # No separate source/dest + {:ok, resp, resp} + else + {:ok, %{status: status} = response} -> + Logger.warning("LibreTranslate: Request rejected: #{inspect(response)}") + {:error, "LibreTranslate request failed (code #{status})"} + + {:error, reason} -> + {:error, reason} + end + end + + @impl Pleroma.Akkoma.Translator + def translate(string, from_language, to_language) do + with {:ok, %{status: 200} = response} <- do_request(string, from_language, to_language), + {:ok, body} <- Jason.decode(response.body) do + %{"translatedText" => translated} = body + + detected = + if Map.has_key?(body, "detectedLanguage") do + get_in(body, ["detectedLanguage", "language"]) + else + from_language + end + + {: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, from_language, to_language) do + url = URI.parse(url()) + url = %{url | path: "/translate"} + + HTTP.post( + to_string(url), + Jason.encode!(%{ + q: string, + source: if(is_nil(from_language), do: "auto", else: from_language), + target: to_language, + format: "html", + api_key: api_key() + }), + [ + {"content-type", "application/json"} + ] + ) + end + + defp do_languages() do + url = URI.parse(url()) + url = %{url | path: "/languages"} + + HTTP.get(to_string(url)) + end +end diff --git a/lib/pleroma/akkoma/translators/translator.ex b/lib/pleroma/akkoma/translators/translator.ex new file mode 100644 index 000000000..93fbeb3b9 --- /dev/null +++ b/lib/pleroma/akkoma/translators/translator.ex @@ -0,0 +1,8 @@ +defmodule Pleroma.Akkoma.Translator do + @callback translate(String.t(), String.t() | nil, String.t()) :: + {:ok, String.t(), String.t()} | {:error, any()} + @callback languages() :: + {:ok, [%{name: String.t(), code: String.t()}], + [%{name: String.t(), code: String.t()}]} + | {:error, any()} +end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index cb619232f..b809f7733 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -63,7 +63,8 @@ defmodule Pleroma.Application 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() ++ @@ -153,7 +154,8 @@ defmodule Pleroma.Application 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/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 @@ defmodule Akkoma.Collections.Fetcher 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 @@ defmodule Akkoma.Collections.Fetcher do 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 @@ defmodule Akkoma.Collections.Fetcher do 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/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 6a3184e6c..81dc847cf 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -38,7 +38,6 @@ defmodule Pleroma.Config.TransferTask do def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do # We need to restart applications for loaded settings take effect - {logger, other} = (Repo.all(ConfigDB) ++ deleted_settings) |> Enum.map(&merge_with_default/1) @@ -85,7 +84,12 @@ defmodule Pleroma.Config.TransferTask do end defp merge_with_default(%{group: group, key: key, value: value} = setting) do - default = Config.Holder.default_config(group, key) + default = + if group == :pleroma do + Config.get([key], Config.Holder.default_config(group, key)) + else + Config.Holder.default_config(group, key) + end merged = cond do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 03e72be58..20acdf86e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -331,9 +331,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp do_unfollow(follower, followed, activity_id, local) when local == true do with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), - {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), {:ok, activity} <- insert(unfollow_data, local), + {:ok, _activity} <- Repo.delete(follow_activity), _ <- notify_and_stream(activity), :ok <- maybe_federate(activity) do {:ok, activity} @@ -349,7 +349,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), {:ok, _activity} <- Repo.delete(follow_activity), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), - unfollow_activity <- remote_unfollow_data(unfollow_data), + unfollow_activity <- make_unfollow_activity(unfollow_data, false), _ <- notify_and_stream(unfollow_activity) do {:ok, unfollow_activity} else @@ -358,12 +358,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - defp remote_unfollow_data(data) do + defp make_unfollow_activity(data, local) do {recipients, _, _} = get_recipients(data) %Activity{ data: data, - local: false, + local: local, actor: data["actor"], recipients: recipients } 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy 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/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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> 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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 78139e5fd..b15be8b22 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -466,18 +466,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do {:ok, activity} end - def update_follow_state( - %Activity{} = activity, - state - ) do - new_data = Map.put(activity.data, "state", state) - changeset = Changeset.change(activity, data: new_data) - - with {:ok, activity} <- Repo.update(changeset) do - {:ok, activity} - end - end - @doc """ Makes a follow activity data for the given follower and followed """ diff --git a/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex new file mode 100644 index 000000000..9983a7e39 --- /dev/null +++ b/lib/pleroma/web/akkoma_api/controllers/translation_controller.ex @@ -0,0 +1,43 @@ +defmodule Pleroma.Web.AkkomaAPI.TranslationController do + use Pleroma.Web, :controller + + alias Pleroma.Web.Plugs.OAuthScopesPlug + + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + + @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} + plug( + OAuthScopesPlug, + %{@unauthenticated_access | scopes: ["read:statuses"]} + when action in [ + :languages + ] + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TranslationOperation + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + @doc "GET /api/v1/akkoma/translation/languages" + def languages(conn, _params) do + with {:ok, source_languages, dest_languages} <- get_languages() do + conn + |> json(%{source: source_languages, target: dest_languages}) + else + e -> IO.inspect(e) + end + end + + defp get_languages do + module = Pleroma.Config.get([:translator, :module]) + + @cachex.fetch!(:translations_cache, "languages:#{module}}", fn _ -> + with {:ok, source_languages, dest_languages} <- module.languages() do + {:ok, source_languages, dest_languages} + else + {:error, err} -> {:ignore, {:error, err}} + end + end) + end +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..5332c9dca 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 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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(), source_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,14 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do ) end + defp language_param do + Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en") + end + + defp source_language_param do + Operation.parameter(:from, :query, :string, "ISO 639 language code", example: "en") + end + defp status_response do Operation.response("Status", "application/json", Status) end @@ -573,4 +597,20 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation 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/api_spec/operations/translate_operation.ex b/lib/pleroma/web/api_spec/operations/translate_operation.ex new file mode 100644 index 000000000..bf0280319 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/translate_operation.ex @@ -0,0 +1,53 @@ +defmodule Pleroma.Web.ApiSpec.TranslationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + @spec languages_operation() :: Operation.t() + def languages_operation() do + %Operation{ + tags: ["Retrieve status translation"], + summary: "Translate status", + description: "View the translation of a given status", + operationId: "AkkomaAPI.TranslationController.languages", + security: [%{"oAuth" => ["read:statuses"]}], + responses: %{ + 200 => + Operation.response("Translation", "application/json", source_dest_languages_schema()) + } + } + end + + defp source_dest_languages_schema do + %Schema{ + type: :object, + required: [:source, :target], + properties: %{ + source: languages_schema(), + target: languages_schema() + } + } + end + + defp languages_schema do + %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + code: %Schema{ + type: :string + }, + name: %Schema{ + type: :string + } + } + } + } + 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..41fbd7acf 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,51 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do ) end + @doc "GET /api/v1/statuses/:id/translations/:language" + def translate(%{assigns: %{user: user}} = conn, %{id: id, language: language} = params) 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"], + Map.get(params, :from, nil), + 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, source_language, target_language, translation_module) do + @cachex.fetch!( + :translations_cache, + "translations:#{status_id}:#{source_language}:#{target_language}", + fn _ -> + value = translation_module.translate(text, source_language, target_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 4cfa8d85c..436519439 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 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView 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): @@ -81,6 +81,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView 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/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 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler 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 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler 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 @@ defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke 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/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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug 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/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 647d99278..175b1c4c0 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -462,6 +462,11 @@ defmodule Pleroma.Web.Router do put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create) end + scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do + pipe_through(:authenticated_api) + get("/translation/languages", TranslationController, :languages) + end + scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) @@ -553,6 +558,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/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 @@ defmodule Pleroma.Web.Streamer do {: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 @@ defmodule Pleroma.Web.Streamer 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 @@ defmodule Pleroma.Web.Streamer 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 f6cdac595..ef038ce74 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Mixfile 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()], @@ -206,7 +206,7 @@ defmodule Pleroma.Mixfile 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/priv/repo/migrations/20220831170605_remove_local_cancelled_follows.exs b/priv/repo/migrations/20220831170605_remove_local_cancelled_follows.exs new file mode 100644 index 000000000..16597f848 --- /dev/null +++ b/priv/repo/migrations/20220831170605_remove_local_cancelled_follows.exs @@ -0,0 +1,22 @@ +defmodule Pleroma.Repo.Migrations.RemoveLocalCancelledFollows do + use Ecto.Migration + + def up do + statement = """ + DELETE FROM + activities + WHERE + (data->>'type') = 'Follow' + AND + (data->>'state') = 'cancelled' + AND + local = true; + """ + + execute(statement) + end + + def down do + :ok + end +end 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

diff --git a/test/mix/tasks/pleroma/relay_test.exs b/test/mix/tasks/pleroma/relay_test.exs index db75b3630..d45690418 100644 --- a/test/mix/tasks/pleroma/relay_test.exs +++ b/test/mix/tasks/pleroma/relay_test.exs @@ -65,7 +65,7 @@ defmodule Mix.Tasks.Pleroma.RelayTest do Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" + assert is_nil(cancelled_activity) [undo_activity] = ActivityPub.fetch_activities([], %{ @@ -78,7 +78,6 @@ defmodule Mix.Tasks.Pleroma.RelayTest do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] refute "#{target_instance}/followers" in User.following(local_user) end @@ -142,7 +141,7 @@ defmodule Mix.Tasks.Pleroma.RelayTest do Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance, "--force"]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) - assert cancelled_activity.data["state"] == "cancelled" + assert is_nil(cancelled_activity) [undo_activity] = ActivityPub.fetch_activities( @@ -152,7 +151,6 @@ defmodule Mix.Tasks.Pleroma.RelayTest do assert undo_activity.data["type"] == "Undo" assert undo_activity.data["actor"] == local_user.ap_id - assert undo_activity.data["object"]["id"] == cancelled_activity.data["id"] refute "#{target_instance}/followers" in User.following(local_user) end end 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 @@ defmodule Akkoma.Collections.FetcherTest 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 @@ defmodule Akkoma.Collections.FetcherTest 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 @@ defmodule Akkoma.Collections.FetcherTest 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 @@ defmodule Akkoma.Collections.FetcherTest 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/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs index c56f20ec5..30cb92fa7 100644 --- a/test/pleroma/config/transfer_task_test.exs +++ b/test/pleroma/config/transfer_task_test.exs @@ -10,13 +10,16 @@ defmodule Pleroma.Config.TransferTaskTest do alias Pleroma.Config.TransferTask - setup do: clear_config(:configurable_from_database, true) + setup do + clear_config(:configurable_from_database, true) + end test "transfer config values from db to env" do refute Application.get_env(:pleroma, :test_key) refute Application.get_env(:idna, :test_key) refute Application.get_env(:quack, :test_key) refute Application.get_env(:postgrex, :test_key) + initial = Application.get_env(:logger, :level) insert(:config, key: :test_key, value: [live: 2, com: 3]) @@ -24,7 +27,7 @@ defmodule Pleroma.Config.TransferTaskTest do insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2]) insert(:config, group: :postgrex, key: :test_key, value: :value) insert(:config, group: :logger, key: :level, value: :debug) - + insert(:config, group: :pleroma, key: :instance, value: [static_dir: "static_dir_from_db"]) TransferTask.start_link([]) assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3] @@ -32,6 +35,7 @@ defmodule Pleroma.Config.TransferTaskTest do assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2] assert Application.get_env(:logger, :level) == :debug assert Application.get_env(:postgrex, :test_key) == :value + assert Application.get_env(:pleroma, :instance)[:static_dir] == "static_dir_from_db" on_exit(fn -> Application.delete_env(:pleroma, :test_key) @@ -39,6 +43,42 @@ defmodule Pleroma.Config.TransferTaskTest do Application.delete_env(:quack, :test_key) Application.delete_env(:postgrex, :test_key) Application.put_env(:logger, :level, initial) + System.delete_env("RELEASE_NAME") + end) + end + + test "transfer task falls back to env before default" do + instance = Application.get_env(:pleroma, :instance) + + insert(:config, key: :instance, value: [name: "wow"]) + clear_config([:instance, :static_dir], "static_dir_from_env") + TransferTask.start_link([]) + + assert Application.get_env(:pleroma, :instance)[:name] == "wow" + assert Application.get_env(:pleroma, :instance)[:static_dir] == "static_dir_from_env" + + on_exit(fn -> + Application.put_env(:pleroma, :instance, instance) + end) + end + + test "transfer task falls back to release defaults if no other values found" do + instance = Application.get_env(:pleroma, :instance) + + System.put_env("RELEASE_NAME", "akkoma") + Pleroma.Config.Holder.save_default() + insert(:config, key: :instance, value: [name: "wow"]) + Application.delete_env(:pleroma, :instance) + + TransferTask.start_link([]) + + assert Application.get_env(:pleroma, :instance)[:name] == "wow" + assert Application.get_env(:pleroma, :instance)[:static_dir] == "/var/lib/akkoma/static" + + on_exit(fn -> + System.delete_env("RELEASE_NAME") + Pleroma.Config.Holder.save_default() + Application.put_env(:pleroma, :instance, instance) end) end 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 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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 @@ defmodule Pleroma.Integration.MastodonWebsocketTest 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/notification_test.exs b/test/pleroma/notification_test.exs index 4354dd2b6..8db208878 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -427,13 +427,12 @@ defmodule Pleroma.NotificationTest do {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) assert FollowingRelationship.following?(user, followed_user) - assert [notification] = Notification.for_user(followed_user) + assert [_notification] = Notification.for_user(followed_user) CommonAPI.unfollow(user, followed_user) {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user) - notification_id = notification.id - assert [%{id: ^notification_id}] = Notification.for_user(followed_user) + assert Enum.count(Notification.for_user(followed_user)) == 1 end test "dismisses the notification on follow request rejection" do diff --git a/test/pleroma/translators/deepl_test.exs b/test/pleroma/translators/deepl_test.exs new file mode 100644 index 000000000..d85bef982 --- /dev/null +++ b/test/pleroma/translators/deepl_test.exs @@ -0,0 +1,146 @@ +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 list supported languages" do + clear_config([:deepl, :tier], :free) + + Tesla.Mock.mock(fn + %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} = 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!([ + %{ + "language" => "BG", + "name" => "Bulgarian", + "supports_formality" => false + }, + %{ + "language" => "CS", + "name" => "Czech", + "supports_formality" => false + } + ]) + } + + %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{ + "language" => "JA", + "name" => "Japanese", + "supports_formality" => false + } + ]) + } + end) + + assert {:ok, [%{code: "JA", name: "Japanese"}], + [%{code: "BG", name: "Bulgarian"}, %{code: "CS", name: "Czech"}]} = + DeepL.languages() + 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("ギュギュ握りつぶしちゃうぞ", nil, "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("ギュギュ握りつぶしちゃうぞ", nil, "en") + end + + test "should assign source language if set" 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 + assert String.contains?(env.body, "source_lang=ja") + + %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("ギュギュ握りつぶしちゃうぞ", "ja", "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("ギュギュ握りつぶしちゃうぞ", nil, "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..3c81c3d76 --- /dev/null +++ b/test/pleroma/translators/libre_translate_test.exs @@ -0,0 +1,137 @@ +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 list supported languages" do + clear_config([:deepl, :tier], :free) + + Tesla.Mock.mock(fn + %{method: :get, url: "http://libre.translate/languages"} = _ -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{ + "code" => "en", + "name" => "English" + }, + %{ + "code" => "ar", + "name" => "Arabic" + } + ]) + } + end) + + assert {:ok, [%{code: "en", name: "English"}, %{code: "ar", name: "Arabic"}], + [%{code: "en", name: "English"}, %{code: "ar", name: "Arabic"}]} = + LibreTranslate.languages() + 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, "source" => "auto"}} = 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("ギュギュ握りつぶしちゃうぞ", nil, "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("ギュギュ握りつぶしちゃうぞ", nil, "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("ギュギュ握りつぶしちゃうぞ", nil, "en") + end + + test "should set a source language if requested" do + Tesla.Mock.mock(fn + %{method: :post, url: "http://libre.translate/translate"} = env -> + assert {:ok, %{"api_key" => nil, "source" => "ja"}} = Jason.decode(env.body) + + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + translatedText: "I will crush you" + }) + } + end) + + assert {:ok, "ja", "I will crush you"} = + LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", "ja", "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("ギュギュ握りつぶしちゃうぞ", nil, "zoop") + end + 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest 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/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 50eff9431..ec562ac7b 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1373,6 +1373,25 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert embedded_object["id"] == follow_activity.data["id"] end + test "it removes the follow activity if it was local" do + follower = insert(:user, local: true) + followed = insert(:user) + + {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed) + {:ok, activity} = ActivityPub.unfollow(follower, followed, nil, true) + + assert activity.data["type"] == "Undo" + assert activity.data["actor"] == follower.ap_id + + follow_activity = Activity.get_by_id(follow_activity.id) + assert is_nil(follow_activity) + assert is_nil(Utils.fetch_latest_follow(follower, followed)) + + # We need to keep our own undo + undo_activity = Activity.get_by_ap_id(activity.data["id"]) + refute is_nil(undo_activity) + end + test "it removes the follow activity if it was remote" do follower = insert(:user, local: false) followed = insert(:user) @@ -1383,9 +1402,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert activity.data["type"] == "Undo" assert activity.data["actor"] == follower.ap_id - activity = Activity.get_by_id(follow_activity.id) - assert is_nil(activity) + follow_activity = Activity.get_by_id(follow_activity.id) + assert is_nil(follow_activity) assert is_nil(Utils.fetch_latest_follow(follower, followed)) + + undo_activity = Activity.get_by_ap_id(activity.data["id"]) + assert is_nil(undo_activity) end end 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 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest 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) 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 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest "@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 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do 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/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 0d88303e3..e45af3aec 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -229,29 +229,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end - describe "update_follow_state/2" do - test "updates the state of the given follow activity" do - user = insert(:user, is_locked: true) - follower = insert(:user) - - {:ok, _, _, follow_activity} = CommonAPI.follow(follower, user) - {:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user) - - data = - follow_activity_two.data - |> Map.put("state", "accept") - - cng = Ecto.Changeset.change(follow_activity_two, data: data) - - {:ok, follow_activity_two} = Repo.update(cng) - - {:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject") - - assert refresh_record(follow_activity).data["state"] == "pending" - assert refresh_record(follow_activity_two).data["state"] == "reject" - end - end - describe "update_element_in_object/3" do test "updates likes" do user = insert(:user) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index fa751bf60..840d74d2f 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1058,24 +1058,23 @@ defmodule Pleroma.Web.CommonAPITest do refute User.subscribed_to?(follower, followed) end - test "cancels a pending follow for a local user" do + test "removes a pending follow for a local user" do follower = insert(:user) followed = insert(:user, is_locked: true) - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) assert User.get_follow_state(follower, followed) == :follow_pending assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert User.get_follow_state(follower, followed) == nil - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)) assert %{ data: %{ "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} + "object" => %{"type" => "Follow"} } } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) end @@ -1084,20 +1083,19 @@ defmodule Pleroma.Web.CommonAPITest do follower = insert(:user) followed = insert(:user, is_locked: true, local: false, ap_enabled: true) - assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = + assert {:ok, follower, followed, %{id: _activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) assert User.get_follow_state(follower, followed) == :follow_pending assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert User.get_follow_state(follower, followed) == nil - assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = - Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) + assert is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)) assert %{ data: %{ "type" => "Undo", - "object" => %{"type" => "Follow", "state" => "cancelled"} + "object" => %{"type" => "Follow"} } } = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) end 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 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do }, "stats" => _, "thumbnail" => from_config_thumbnail, - "languages" => _, + "languages" => ["en", "ja"], "registrations" => _, "approval_required" => _, "poll_limits" => _, 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..f76ab3d0d 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,110 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest 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 "listing languages", %{conn: conn} do + Tesla.Mock.mock_global(fn + %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=source"} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{language: "en", name: "English"} + ]) + } + + %{method: :get, url: "https://api-free.deepl.com/v2/languages?type=target"} -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!([ + %{language: "ja", name: "Japanese"} + ]) + } + end) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/akkoma/translation/languages") + + response = json_response_and_validate_schema(conn, 200) + + assert %{ + "source" => [%{"code" => "en", "name" => "English"}], + "target" => [%{"code" => "ja", "name" => "Japanese"}] + } = response + 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 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 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do 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/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 @@ defmodule Pleroma.Web.StreamerTest do 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/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 @@ defmodule HttpRequestMock 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 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 @@ defmodule Pleroma.Integration.WebsocketClient 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