diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d8c3d8e..0f3447069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `:reject_deletes` group to SimplePolicy - MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances - Support pagination in emoji packs API (for packs and for files in pack) +- Support for viewing instances favicons next to posts and accounts
API Changes @@ -65,6 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add support for filtering replies in public and home timelines. - Mastodon API: Support for `bot` field in `/api/v1/accounts/update_credentials`. - Mastodon API: Support irreversible property for filters. +- Mastodon API: Add pleroma.favicon field to accounts. - Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoint for status view. - OTP: Add command to reload emoji packs diff --git a/config/config.exs b/config/config.exs index 458d3a99a..3577cd101 100644 --- a/config/config.exs +++ b/config/config.exs @@ -706,6 +706,8 @@ config :ex_aws, http_client: Pleroma.HTTP.ExAws +config :pleroma, :instances_favicons, enabled: false + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/description.exs b/config/description.exs index 705ba83d0..03b84bfc8 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3448,5 +3448,18 @@ suggestions: [false] } ] + }, + %{ + group: :pleroma, + key: :instances_favicons, + type: :group, + description: "Control favicons for instances", + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Allow/disallow displaying and getting instances favicons" + } + ] } ] diff --git a/config/test.exs b/config/test.exs index e38b9967d..e6596e0bc 100644 --- a/config/test.exs +++ b/config/test.exs @@ -111,6 +111,8 @@ config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true +config :pleroma, :instances_favicons, enabled: true + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md index 29141ed0c..03c7f4608 100644 --- a/docs/API/differences_in_mastoapi_responses.md +++ b/docs/API/differences_in_mastoapi_responses.md @@ -71,6 +71,7 @@ Has these additional fields under the `pleroma` object: - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_notifications_count`: The count of unread notifications. Only returned to the account owner. - `notification_settings`: object, can be absent. See `/api/pleroma/notification_settings` for the parameters/keys returned. +- `favicon`: nullable URL string, Favicon image of the user's instance ### Source diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index d0a57928c..d775534b6 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -988,3 +988,9 @@ Note: setting `restrict_unauthenticated/timelines/local` to `true` has no practi ## Pleroma.Web.ApiSpec.CastAndValidate * `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`. + +## :instances_favicons + +Control favicons for instances. + +* `enabled`: Allow/disallow displaying and getting instances favicons diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 74458c09a..a1f935232 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do schema "instances" do field(:host, :string) field(:unreachable_since, :naive_datetime_usec) + field(:favicon, :string) + field(:favicon_updated_at, :naive_datetime) timestamps() end @@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do def changeset(struct, params \\ %{}) do struct - |> cast(params, [:host, :unreachable_since]) + |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at]) |> validate_required([:host]) |> unique_constraint(:host) end @@ -120,4 +122,48 @@ defp parse_datetime(datetime) when is_binary(datetime) do end defp parse_datetime(datetime), do: datetime + + def get_or_update_favicon(%URI{host: host} = instance_uri) do + existing_record = Repo.get_by(Instance, %{host: host}) + now = NaiveDateTime.utc_now() + + if existing_record && existing_record.favicon_updated_at && + NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do + existing_record.favicon + else + favicon = scrape_favicon(instance_uri) + + if existing_record do + existing_record + |> changeset(%{favicon: favicon, favicon_updated_at: now}) + |> Repo.update() + else + %Instance{} + |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now}) + |> Repo.insert() + end + + favicon + end + end + + defp scrape_favicon(%URI{} = instance_uri) do + try do + with {:ok, %Tesla.Env{body: html}} <- + Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), + favicon_rel <- + html + |> Floki.parse_document!() + |> Floki.attribute("link[rel=icon]", "href") + |> List.first(), + favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), + true <- is_binary(favicon) do + favicon + else + _ -> nil + end + rescue + _ -> nil + end + end end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 84f18f1b6..e6f163cb7 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -102,6 +102,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do type: :object, description: "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" + }, + favicon: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "Favicon image of the user's instance" } } }, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a6e64b4ab..2feba4778 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -204,6 +204,18 @@ defp do_render("show.json", %{user: user} = opts) do %{} end + favicon = + if Pleroma.Config.get([:instances_favicons, :enabled]) do + user + |> Map.get(:ap_id, "") + |> URI.parse() + |> URI.merge("/") + |> Pleroma.Instances.Instance.get_or_update_favicon() + |> MediaProxy.url() + else + nil + end + %{ id: to_string(user.id), username: username_from_nickname(user.nickname), @@ -245,7 +257,8 @@ defp do_render("show.json", %{user: user} = opts) do hide_favorites: user.hide_favorites, relationship: relationship, skip_thread_containment: user.skip_thread_containment, - background_image: image_url(user.background) |> MediaProxy.url() + background_image: image_url(user.background) |> MediaProxy.url(), + favicon: favicon } } |> maybe_put_role(user, opts[:for]) diff --git a/priv/repo/migrations/20200707112859_instances_add_favicon.exs b/priv/repo/migrations/20200707112859_instances_add_favicon.exs new file mode 100644 index 000000000..5538749dc --- /dev/null +++ b/priv/repo/migrations/20200707112859_instances_add_favicon.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.InstancesAddFavicon do + use Ecto.Migration + + def change do + alter table(:instances) do + add(:favicon, :string) + add(:favicon_updated_at, :naive_datetime) + end + end +end diff --git a/test/fixtures/tesla_mock/https___osada.macgirvin.com.html b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html new file mode 100644 index 000000000..880273d74 --- /dev/null +++ b/test/fixtures/tesla_mock/https___osada.macgirvin.com.html @@ -0,0 +1,301 @@ + + + + Osada + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

Welcome to Osada

+ +
+
+
+ + + +
+
+ + +
+
+ +
+ +
+ +
+ +
+ Remote Authentication +
+ +
+ + + +
+
+ +
+ + + + + \ No newline at end of file diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index da04ac6f1..19a202654 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -1342,6 +1342,18 @@ def get("https://relay.mastodon.host/actor", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/relay/relay.json")}} end + def get("http://localhost:4001/", _, "", Accept: "text/html") do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/7369654.html")}} + end + + def get("https://osada.macgirvin.com/", _, "", Accept: "text/html") do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___osada.macgirvin.com.html") + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{ diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 3e2e780e3..f5bfc9c67 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do use Pleroma.DataCase + alias Pleroma.Config alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI @@ -18,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do :ok end + setup do: clear_config([:instances_favicons, :enabled]) + test "Represent a user account" do background_image = %{ "url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}] @@ -75,6 +78,8 @@ test "Represent a user account" do pleroma: %{ ap_id: user.ap_id, background_image: "https://example.com/images/asuka_hospital.png", + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", confirmation_pending: false, tags: [], is_admin: false, @@ -92,6 +97,23 @@ test "Represent a user account" do assert expected == AccountView.render("show.json", %{user: user}) end + test "Favicon is nil when :instances_favicons is disabled" do + user = insert(:user) + + Config.put([:instances_favicons, :enabled], true) + + assert %{ + pleroma: %{ + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png" + } + } = AccountView.render("show.json", %{user: user}) + + Config.put([:instances_favicons, :enabled], false) + + assert %{pleroma: %{favicon: nil}} = AccountView.render("show.json", %{user: user}) + end + test "Represent the user account for the account owner" do user = insert(:user) @@ -152,6 +174,8 @@ test "Represent a Service(bot) account" do pleroma: %{ ap_id: user.ap_id, background_image: nil, + favicon: + "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", confirmation_pending: false, tags: [], is_admin: false,