display federation restrictions

This commit is contained in:
Tao Bror Bojlén 2019-08-29 17:54:34 +01:00
parent f572cd937e
commit f134941eb2
No known key found for this signature in database
GPG key ID: C6EC7AAB905F9E6F
16 changed files with 185 additions and 31 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 && (
<ul>
{domains.sort().map(domain => (
<li key={domain}>
<Link to={`/instance/${domain}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
{domain}
</Link>
</li>
))}
</ul>
);
interface IFederationTabProps {
restrictions?: IFederationRestrictions;
}
const FederationTab: React.FC<IFederationTabProps> = ({ 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 && (
<>
<H3>Blocked instances</H3>
{rejectsList}
</>
)}
{reportsRemovalList && (
<>
<H3>Reports ignored</H3>
{reportsRemovalList}
</>
)}
{mediaRemovalsList && (
<>
<H3>Media removed</H3>
{mediaRemovalsList}
</>
)}
{mediaNsfwsList && (
<>
<H3>Media marked as NSFW</H3>
{mediaNsfwsList}
</>
)}
{federatedTimelineRemovalsList && (
<>
<H3>Hidden from federated timeline</H3>
{federatedTimelineRemovalsList}
</>
)}
{bannerRemovalsList && (
<>
<H3>Banners removed</H3>
{bannerRemovalsList}
</>
)}
{avatarRemovalsList && (
<>
<H3>Avatars removed</H3>
{avatarRemovalsList}
</>
)}
{acceptedList && (
<>
<H3>Whitelisted</H3>
{acceptedList}
</>
)}
</>
);
};
export default FederationTab;

View file

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

View file

@ -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<IInstanceScreenProps, IInst
private renderTabs = () => {
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<IInstanceScreenProps, IInst
<Tab id="description" title="Description" panel={this.renderDescription()} />
)}
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
{federationRestrictions && Object.keys(federationRestrictions).length > 0 && (
<Tab
id="federationRestrictions"
title="Federation"
panel={<FederationTab restrictions={federationRestrictions} />}
/>
)}
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
</StyledTabs>
@ -389,8 +398,8 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
<div>
<NeighborsCallout icon={IconNames.INFO_SIGN} title="Warning">
<p>
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.
</p>
</NeighborsCallout>
<p className={Classes.TEXT_MUTED}>

View file

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