Merge branch 'develop' into remote-emoji-reactions

This commit is contained in:
FloatingGhost 2022-09-04 19:14:00 +01:00
commit e5fda3841c
64 changed files with 1511 additions and 189 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -202,6 +202,8 @@ incorrect timestamps. You should have ntpd running.
* <https://catgirl.science>
{! installation/frontends.include !}
#### Further reading
{! installation/further_reading.include !}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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!")

View File

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

View File

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

View File

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

View File

@ -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"},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = %{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" => _,

View File

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

View File

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

View File

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

View File

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

View File

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