diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1ec65..a4005b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add support for logging in via an ActivityPub direct message to the instance admin. - Added option to hide edges between instances if there are only mentions in one direction (off by default). -- Added note to neighbors tab to make it explicit that blocked instances _may_ appear. +- Added note to neighbors tab to make it explicit that blocked instances may appear. +- Added federation tab that shows federation restrictions (only available for some Pleroma instances). ### Changed diff --git a/backend/lib/backend/api.ex b/backend/lib/backend/api.ex index b0ffe21..a5e492a 100644 --- a/backend/lib/backend/api.ex +++ b/backend/lib/backend/api.ex @@ -19,10 +19,11 @@ defmodule Backend.Api do |> Repo.get_by(domain: domain) end - @spec get_instance_with_peers(String.t()) :: Instance.t() | nil - def get_instance_with_peers(domain) do + @spec get_instance_with_relationships(String.t()) :: Instance.t() | nil + def get_instance_with_relationships(domain) do Instance |> preload(:peers) + |> preload(:federation_restrictions) |> Repo.get_by(domain: domain) end diff --git a/backend/lib/backend/crawler/api_crawler.ex b/backend/lib/backend/crawler/api_crawler.ex index c6d0164..5564881 100644 --- a/backend/lib/backend/crawler/api_crawler.ex +++ b/backend/lib/backend/crawler/api_crawler.ex @@ -15,6 +15,8 @@ defmodule Backend.Crawler.ApiCrawler do # {domain_mentioned, count} @type instance_interactions :: %{String.t() => integer} + # {domain, type} e.g. {"gab.com", "reject"} + @type federation_restriction :: {String.t(), String.t()} @type instance_type :: :mastodon | :pleroma | :gab | :misskey | :gnusocial @@ -27,7 +29,7 @@ defmodule Backend.Crawler.ApiCrawler do :interactions, :statuses_seen, :instance_type, - :blocked_domains + :federation_restrictions ] @type t() :: %__MODULE__{ @@ -39,7 +41,7 @@ defmodule Backend.Crawler.ApiCrawler do interactions: instance_interactions, statuses_seen: integer, instance_type: instance_type | nil, - blocked_domains: [String.t()] + federation_restrictions: [federation_restriction] } @empty_result %{ @@ -51,7 +53,7 @@ defmodule Backend.Crawler.ApiCrawler do interactions: %{}, statuses_seen: 0, instance_type: nil, - blocked_domains: [] + federation_restrictions: [] } @doc """ diff --git a/backend/lib/backend/crawler/crawler.ex b/backend/lib/backend/crawler/crawler.ex index 4b8b1b0..608dd75 100644 --- a/backend/lib/backend/crawler/crawler.ex +++ b/backend/lib/backend/crawler/crawler.ex @@ -227,7 +227,9 @@ defmodule Backend.Crawler do new_instances = peers_domains - |> list_union(result.blocked_domains) + |> list_union( + Enum.map(result.federation_restrictions, fn {domain, _restriction_type} -> domain end) + ) |> Enum.map(&%{domain: &1, inserted_at: now, updated_at: now, next_crawl: now}) Instance @@ -282,8 +284,7 @@ defmodule Backend.Crawler do |> Repo.all() wanted_restrictions_set = - result.blocked_domains - |> Enum.map(&{&1, "reject"}) + result.federation_restrictions |> MapSet.new() current_restrictions_set = MapSet.new(current_restrictions) diff --git a/backend/lib/backend/crawler/crawlers/nodeinfo.ex b/backend/lib/backend/crawler/crawlers/nodeinfo.ex index 413d8d2..df6f237 100644 --- a/backend/lib/backend/crawler/crawlers/nodeinfo.ex +++ b/backend/lib/backend/crawler/crawlers/nodeinfo.ex @@ -85,16 +85,7 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do status_count: get_in(nodeinfo, ["usage", "localPosts"]), instance_type: type, version: get_in(nodeinfo, ["software", "version"]), - blocked_domains: - get_in(nodeinfo, ["metadata", "federation", "mrf_simple", "reject"]) - |> (fn b -> - if b == nil do - [] - else - b - end - end).() - |> Enum.map(&clean_domain(&1)) + federation_restrictions: get_federation_restrictions(nodeinfo) } ) else @@ -112,4 +103,37 @@ defmodule Backend.Crawler.Crawlers.Nodeinfo do version = String.slice(schema_url, (String.length(schema_url) - 3)..-1) Enum.member?(["1.0", "1.1", "2.0"], version) end + + @spec get_federation_restrictions(any()) :: [ApiCrawler.federation_restriction()] + defp get_federation_restrictions(nodeinfo) do + mrf_simple = get_in(nodeinfo, ["metadata", "federation", "mrf_simple"]) + quarantined_domains = get_in(nodeinfo, ["metadata", "federation", "quarantined_instances"]) + + quarantined_domains = + if quarantined_domains == nil do + [] + else + Enum.map(quarantined_domains, fn domain -> {domain, "quarantine"} end) + end + + if mrf_simple != nil do + mrf_simple + |> Map.take([ + "report_removal", + "reject", + "media_removal", + "media_nsfw", + "federated_timeline_removal", + "banner_removal", + "avatar_removal", + "accept" + ]) + |> Enum.flat_map(fn {type, domains} -> + Enum.map(domains, fn domain -> {domain, type} end) + end) + |> Enum.concat(quarantined_domains) + else + quarantined_domains + end + end end diff --git a/backend/lib/backend/instance.ex b/backend/lib/backend/instance.ex index 468e910..fa351f5 100644 --- a/backend/lib/backend/instance.ex +++ b/backend/lib/backend/instance.ex @@ -32,6 +32,10 @@ defmodule Backend.Instance do foreign_key: :source_domain, references: :domain + has_many :federation_restrictions, Backend.FederationRestriction, + foreign_key: :source_domain, + references: :domain + timestamps() end diff --git a/backend/lib/backend_web/controllers/admin_controller.ex b/backend/lib/backend_web/controllers/admin_controller.ex index 221e35d..66ff536 100644 --- a/backend/lib/backend_web/controllers/admin_controller.ex +++ b/backend/lib/backend_web/controllers/admin_controller.ex @@ -21,7 +21,7 @@ defmodule BackendWeb.AdminController do # Make sure to update ElasticSearch so that the instance is no longer returned in search results es_instance = - Api.get_instance_with_peers(domain) + Api.get_instance(domain) |> Map.put(:opt_in, opt_in) |> Map.put(:opt_out, opt_out) diff --git a/backend/lib/backend_web/controllers/instance_controller.ex b/backend/lib/backend_web/controllers/instance_controller.ex index 294597d..40c35c9 100644 --- a/backend/lib/backend_web/controllers/instance_controller.ex +++ b/backend/lib/backend_web/controllers/instance_controller.ex @@ -26,7 +26,7 @@ defmodule BackendWeb.InstanceController do end def show(conn, %{"id" => domain}) do - instance = Cache.get_instance_with_peers(domain) + instance = Cache.get_instance_with_relationships(domain) if instance == nil or instance.opt_out == true do send_resp(conn, 404, "Not found") diff --git a/backend/lib/backend_web/views/instance_view.ex b/backend/lib/backend_web/views/instance_view.ex index c5844a9..6ed8468 100644 --- a/backend/lib/backend_web/views/instance_view.ex +++ b/backend/lib/backend_web/views/instance_view.ex @@ -80,6 +80,13 @@ defmodule BackendWeb.InstanceView do instance.peers |> Enum.filter(fn peer -> not peer.opt_out end) + federation_restrictions = + instance.federation_restrictions + |> Enum.reduce(%{}, fn %{target_domain: domain, type: type}, acc -> + Map.update(acc, type, [domain], fn curr_domains -> [domain | curr_domains] end) + end) + |> Recase.Enumerable.convert_keys(&Recase.to_camel(&1)) + %{ name: instance.domain, description: instance.description, @@ -89,6 +96,7 @@ defmodule BackendWeb.InstanceView do statusCount: instance.status_count, domainCount: length(instance.peers), peers: render_many(filtered_peers, InstanceView, "peer.json"), + federationRestrictions: federation_restrictions, lastUpdated: last_updated, status: "success", type: instance.type, diff --git a/backend/lib/graph/cache.ex b/backend/lib/graph/cache.ex index 8d05827..a905f81 100644 --- a/backend/lib/graph/cache.ex +++ b/backend/lib/graph/cache.ex @@ -38,17 +38,17 @@ defmodule Graph.Cache do end end - @spec get_instance_with_peers(String.t()) :: Instance.t() - def get_instance_with_peers(domain) do + @spec get_instance_with_relationships(String.t()) :: Instance.t() + def get_instance_with_relationships(domain) do key = "instance_" <> domain case Cache.get(key) do nil -> Appsignal.increment_counter("instance_cache.misses", 1) Logger.debug("Instance cache: miss") - instance = Api.get_instance_with_peers(domain) - # Cache for one minute - Cache.set(key, instance, ttl: 60) + instance = Api.get_instance_with_relationships(domain) + # Cache for five minutes + Cache.set(key, instance, ttl: 300) instance data -> @@ -81,8 +81,8 @@ defmodule Graph.Cache do ) |> Repo.one() - # Cache for one minute - Cache.set(key, crawl, ttl: 60) + # Cache for five minutes + Cache.set(key, crawl, ttl: 300) data -> Appsignal.increment_counter("most_recent_crawl_cache.hits", 1) diff --git a/backend/mix.exs b/backend/mix.exs index c10edb4..7aa7a3d 100644 --- a/backend/mix.exs +++ b/backend/mix.exs @@ -66,7 +66,8 @@ defmodule Backend.MixProject do {:nebulex, "~> 1.1"}, {:hunter, "~> 0.5.1"}, {:poison, "~> 4.0", override: true}, - {:scrivener_ecto, "~> 2.2"} + {:scrivener_ecto, "~> 2.2"}, + {:recase, "~> 0.6.0"} ] end diff --git a/backend/mix.lock b/backend/mix.lock index df47d89..6143dde 100644 --- a/backend/mix.lock +++ b/backend/mix.lock @@ -50,6 +50,7 @@ "public_suffix": {:hex, :public_suffix, "0.6.0", "100cfe86f13f9f6f0cf67e743b1b83c78dd1223a2c422fa03ebf4adff514cbc3", [:mix], [{:idna, ">= 1.2.0 and < 6.0.0", [hex: :idna, repo: "hexpm", optional: false]}], "hexpm"}, "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm"}, "scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"}, "scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"}, "shards": {:hex, :shards, "0.6.0", "678d292ad74a4598a872930f9b12251f43e97f6050287f1fb712fbfd3d282f75", [:make, :rebar3], [], "hexpm"}, diff --git a/frontend/src/components/organisms/FederationTab.tsx b/frontend/src/components/organisms/FederationTab.tsx new file mode 100644 index 0000000..f17b613 --- /dev/null +++ b/frontend/src/components/organisms/FederationTab.tsx @@ -0,0 +1,89 @@ +import { Classes, H3 } from "@blueprintjs/core"; +import React from "react"; +import { Link } from "react-router-dom"; +import { IFederationRestrictions } from "../../redux/types"; + +const maybeGetList = (domains?: string[]) => + domains && ( + + ); + +interface IFederationTabProps { + restrictions?: IFederationRestrictions; +} +const FederationTab: React.FC = ({ restrictions }) => { + if (!restrictions) { + return null; + } + + const reportsRemovalList = maybeGetList(restrictions.reportRemoval); + const rejectsList = maybeGetList(restrictions.reject); + const mediaRemovalsList = maybeGetList(restrictions.mediaRemoval); + const mediaNsfwsList = maybeGetList(restrictions.mediaNsfw); + const federatedTimelineRemovalsList = maybeGetList(restrictions.federatedTimelineRemoval); + const bannerRemovalsList = maybeGetList(restrictions.bannerRemoval); + const avatarRemovalsList = maybeGetList(restrictions.avatarRemoval); + const acceptedList = maybeGetList(restrictions.accept); + + return ( + <> + {rejectsList && ( + <> +

Blocked instances

+ {rejectsList} + + )} + {reportsRemovalList && ( + <> +

Reports ignored

+ {reportsRemovalList} + + )} + {mediaRemovalsList && ( + <> +

Media removed

+ {mediaRemovalsList} + + )} + {mediaNsfwsList && ( + <> +

Media marked as NSFW

+ {mediaNsfwsList} + + )} + {federatedTimelineRemovalsList && ( + <> +

Hidden from federated timeline

+ {federatedTimelineRemovalsList} + + )} + {bannerRemovalsList && ( + <> +

Banners removed

+ {bannerRemovalsList} + + )} + {avatarRemovalsList && ( + <> +

Avatars removed

+ {avatarRemovalsList} + + )} + {acceptedList && ( + <> +

Whitelisted

+ {acceptedList} + + )} + + ); +}; +export default FederationTab; diff --git a/frontend/src/components/organisms/index.ts b/frontend/src/components/organisms/index.ts index ec8acc3..9a872ea 100644 --- a/frontend/src/components/organisms/index.ts +++ b/frontend/src/components/organisms/index.ts @@ -3,3 +3,4 @@ export { default as Nav } from "./Nav"; export { default as SidebarContainer } from "./SidebarContainer"; export { default as SearchFilters } from "./SearchFilters"; export { default as InstanceTable } from "./InstanceTable"; +export { default as FederationTab } from "./FederationTab"; diff --git a/frontend/src/components/screens/InstanceScreen.tsx b/frontend/src/components/screens/InstanceScreen.tsx index ac0561f..53dae65 100644 --- a/frontend/src/components/screens/InstanceScreen.tsx +++ b/frontend/src/components/screens/InstanceScreen.tsx @@ -32,6 +32,7 @@ import { IAppState, IGraph, IGraphResponse, IInstanceDetails } from "../../redux import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util"; import { InstanceType } from "../atoms"; import { Cytoscape, ErrorState } from "../molecules/"; +import { FederationTab } from "../organisms"; const InstanceScreenContainer = styled.div` margin-bottom: auto; @@ -192,6 +193,7 @@ class InstanceScreenImpl extends React.PureComponent { const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0; + const federationRestrictions = this.props.instanceDetails && this.props.instanceDetails.federationRestrictions; const hasLocalGraph = !!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0; @@ -212,6 +214,13 @@ class InstanceScreenImpl extends React.PureComponent )} {this.shouldRenderStats() && } + {federationRestrictions && Object.keys(federationRestrictions).length > 0 && ( + } + /> + )} @@ -389,8 +398,8 @@ class InstanceScreenImpl extends React.PureComponent

- Instances that {this.props.instanceName} has blocked may appear on this list. This can happen if users on a - blocked instance attempted to mention someone on {this.props.instanceName}. + Instances that {this.props.instanceName} has blocked may appear on this list and vice versa. This can happen + if users attempt to mention someone on an instance that has blocked them.

diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts index 40a98af..e76fdec 100644 --- a/frontend/src/redux/types.ts +++ b/frontend/src/redux/types.ts @@ -41,6 +41,17 @@ export interface ISearchResultInstance { type?: string; } +export interface IFederationRestrictions { + reportRemoval?: string[]; + reject?: string[]; + mediaRemoval?: string[]; + mediaNsfw?: string[]; + federatedTimelineRemoval?: string[]; + bannerRemoval?: string[]; + avatarRemoval?: string[]; + accept?: string[]; +} + export interface IInstanceDetails { name: string; description?: string; @@ -50,6 +61,7 @@ export interface IInstanceDetails { statusCount?: number; domainCount?: number; peers?: IPeer[]; + federationRestrictions: IFederationRestrictions; lastUpdated?: string; status: string; type?: string;