2.8 KiB · text 5af55d9
defmodule Mix.Tasks.Gitgud.Federation.SuggestBlocks do
@shortdoc "Ingest peer block lists into the suggested-blocks queue"
@moduledoc """
Fetches one or more peer block lists and stages each entry in the
`suggested_instance_blocks` queue for admin review. Never applies
blocks directly — see the module doc on
`GitGud.Federation.SuggestedBlocks` for the design rationale
(defederation cascades are deliberately declined).
## Usage
mix gitgud.federation.suggest_blocks <peer-host>=<url> [<peer-host>=<url> ...]
Each `peer-host=url` pair:
* `peer-host` is the moderating instance's hostname (used in the
aggregated row to show "blocked by N peers")
* `url` is a URL returning a list — one host per line, or
`host,reason` CSV, blank lines + `#` comments ignored.
Plain `path=file:///path/to/list.txt` is also accepted for
testing or air-gapped operation.
## Options
* `--dry-run` — parse + count + log but don't write to DB.
## Example
mix gitgud.federation.suggest_blocks \\
coop.example=https://coop.example/_lists/blocks.csv \\
peer.example=https://peer.example/_lists/blocks.csv
"""
use Mix.Task
alias GitGud.Federation.SuggestedBlocks
@impl Mix.Task
def run(args) do
{opts, pairs, _} = OptionParser.parse(args, switches: [dry_run: :boolean])
if pairs == [] do
Mix.shell().error("usage: mix gitgud.federation.suggest_blocks peer-host=url ...")
exit({:shutdown, 1})
end
Mix.Task.run("app.start")
Enum.each(pairs, fn pair ->
case String.split(pair, "=", parts: 2) do
[peer_host, url] -> ingest_one(peer_host, url, opts)
_ -> Mix.shell().error("ignoring malformed pair: #{pair}")
end
end)
end
defp ingest_one(peer_host, url, opts) do
case fetch(url) do
{:ok, body} ->
entries = SuggestedBlocks.parse_list(body)
{created, updated} = SuggestedBlocks.ingest(entries, peer_host, opts)
Mix.shell().info(
"[#{peer_host}] #{length(entries)} parsed · #{created} new · #{updated} updated" <>
if Keyword.get(opts, :dry_run, false), do: " (dry-run)", else: ""
)
{:error, reason} ->
Mix.shell().error("[#{peer_host}] fetch failed: #{inspect(reason)}")
end
end
defp fetch("file://" <> path) do
File.read(path)
end
defp fetch(url) when is_binary(url) do
case Req.get(url, retry: false, receive_timeout: 30_000) do
{:ok, %Req.Response{status: 200, body: body}} when is_binary(body) ->
{:ok, body}
{:ok, %Req.Response{status: 200, body: body}} when is_list(body) or is_map(body) ->
{:ok, inspect(body)}
{:ok, %Req.Response{status: status}} ->
{:error, {:http, status}}
err ->
err
end
end
end