Merge branch 'develop' into remote-emoji-reactions
This commit is contained in:
commit
e5fda3841c
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
## Conclusion
|
||||
|
||||
Restart nginx with `# service nginx restart` and you should be up and running.
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -293,6 +293,8 @@ akkoma$ MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --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 !}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -202,6 +202,8 @@ incorrect timestamps. You should have ntpd running.
|
|||
|
||||
* <https://catgirl.science>
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 <username> <your@emailaddress> --admin
|
||||
```
|
||||
|
||||
{! installation/frontends.include !}
|
||||
|
||||
#### Further reading
|
||||
|
||||
{! installation/further_reading.include !}
|
||||
|
|
|
@ -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 !}
|
||||
|
|
|
@ -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 !}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
4
mix.exs
4
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
|
||||
|
||||
|
|
2
mix.lock
2
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"},
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -6,7 +6,18 @@
|
|||
<body>
|
||||
<h3>Welcome to Akkoma!</h3>
|
||||
<p>If you're seeing this page, your server works!</p>
|
||||
<p>In order to get a frontend to show here, you'll need to set up <code>:pleroma, :frontends, primary</code> and install your frontend of choice</p>
|
||||
<a href="https://docs.akkoma.dev/stable/configuration/cheatsheet/#frontend-management">Documentation</a>
|
||||
<p>In order to get a frontend to show here, you'll need to set up <code>:pleroma, :frontends, primary</code> and install your frontend of choice, in most cases this will just be:</p>
|
||||
<pre>
|
||||
<code lang="bash">
|
||||
# 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
|
||||
</code>
|
||||
</pre>
|
||||
<p><a href="https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/">Installation Command Documentation</a></p>
|
||||
<p><a href="https://docs.akkoma.dev/stable/configuration/cheatsheet/#frontend-management">Config Documentation</a></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 = %{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -146,4 +146,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
"<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{local_user.id}\" href=\"#{local_user.ap_id}\" rel=\"ugc\">@<span>akkoma_user</span></a></span>"
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" => _,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue