defmodule TribeOne.TribesPlugin.Trust do @moduledoc false alias TribeOne.TribesPlugin.Trust.Domain @federation_capabilities [ "org.tribes.federation.hello@1", "org.tribes.federation.relationship.status@1", "org.tribes.trust.attest@1" ] def federation_capabilities, do: @federation_capabilities def federation_hidden? do case Tribes.ConfigStore.get_for_plugin("tribe-one-trust", "federation.hidden", default: false) do {:ok, value} -> value in [true, "true", "1", "on"] {:error, _reason} -> false end end def set_federation_hidden?(value, opts \\ []) when is_boolean(value) do Tribes.ConfigStore.put_for_plugin("tribe-one-trust", "federation.hidden", value, opts) end def receive_hello(attrs, opts \\ []) when is_map(attrs) do with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs), {:ok, relationship_attrs} <- normalize_inbound_relationship_attrs(attrs), {:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)), {:ok, relationship} <- upsert_inbound_relationship(tribe.pubkey, relationship_attrs, opts) do {:ok, %{tribe: tribe, relationship: relationship}} end end def observe_remote_tribe(attrs, opts \\ []) when is_map(attrs) do with {:ok, tribe_attrs} <- normalize_remote_tribe_attrs(attrs), {:ok, relationship_attrs} <- normalize_observed_relationship_attrs(attrs), {:ok, tribe} <- Domain.upsert_remote_tribe(tribe_attrs, default_trust_opts(opts)), {:ok, relationship} <- ensure_observed_relationship(tribe.pubkey, relationship_attrs, opts) do {:ok, %{tribe: tribe, relationship: relationship}} end end def list_relationship_rows(opts \\ []) do with {:ok, tribes} <- Domain.list_remote_tribes(default_trust_opts(opts)), {:ok, relationships} <- Domain.list_tribe_relationships(default_trust_opts(opts)) do tribes_by_pubkey = Map.new(tribes, &{&1.pubkey, &1}) rows = relationships |> Enum.map(fn relationship -> %{ tribe: Map.get(tribes_by_pubkey, relationship.remote_tribe_pubkey), relationship: relationship } end) |> Enum.sort_by(fn %{relationship: relationship} -> {status_rank(relationship.status), relationship.updated_at || relationship.inserted_at} end) {:ok, rows} end end def access_trust_facts(%{type: "tribe", id: pubkey}), do: access_trust_facts(pubkey) def access_trust_facts(%{"type" => "tribe", "id" => pubkey}), do: access_trust_facts(pubkey) def access_trust_facts(pubkey) when is_binary(pubkey) do with {:ok, pubkey} <- normalize_pubkey(pubkey), {:ok, relationship} <- Domain.get_tribe_relationship( pubkey, Keyword.put(default_trust_opts([]), :not_found_error?, false) ) do {:ok, %{ status: relationship_status(relationship), trust_score: relationship_trust_score(relationship), granted_capabilities: relationship_capabilities(relationship) }} end end def access_trust_facts(_subject), do: {:error, :unsupported_subject} def update_relationship(remote_pubkey, attrs, opts \\ []) when is_binary(remote_pubkey) do with {:ok, pubkey} <- normalize_pubkey(remote_pubkey), {:ok, existing} <- Domain.get_tribe_relationship( pubkey, Keyword.put(default_trust_opts(opts), :not_found_error?, false) ), {:ok, attrs} <- normalize_relationship_update(attrs, existing) do Domain.upsert_tribe_relationship(attrs, default_trust_opts(opts)) end end defp upsert_inbound_relationship(remote_pubkey, attrs, opts) do existing = case Domain.get_tribe_relationship( remote_pubkey, Keyword.put(default_trust_opts(opts), :not_found_error?, false) ) do {:ok, relationship} -> relationship {:error, _reason} -> nil end status = case existing do %{status: status} when status in [:active, :blocked, :revoked] -> status _other -> :pending_inbound end attrs |> Map.merge(%{remote_tribe_pubkey: remote_pubkey, status: status}) |> Domain.upsert_tribe_relationship(default_trust_opts(opts)) end defp ensure_observed_relationship(remote_pubkey, attrs, opts) do case Domain.get_tribe_relationship( remote_pubkey, Keyword.put(default_trust_opts(opts), :not_found_error?, false) ) do {:ok, nil} -> Domain.upsert_tribe_relationship( Map.merge( %{ remote_tribe_pubkey: remote_pubkey, status: :observed, requested_capabilities: [], granted_capabilities: [], trust_score: 0 }, attrs ), default_trust_opts(opts) ) {:ok, relationship} -> {:ok, relationship} {:error, _reason} = error -> error end end defp normalize_remote_tribe_attrs(attrs) do with {:ok, pubkey} <- normalize_pubkey(attrs["from_tribe"] || attrs["pubkey"] || attrs[:pubkey]) do {:ok, %{ pubkey: pubkey, name: normalize_optional_string(attrs["name"] || attrs[:name]) || pubkey, description: normalize_optional_string(attrs["description"] || attrs[:description]), last_seen_at: DateTime.utc_now() }} end end defp normalize_inbound_relationship_attrs(attrs) do with {:ok, requested_capabilities} <- normalize_string_list( attrs["requested_capabilities"] || attrs[:requested_capabilities] || [] ), {:ok, remote_relay_urls} <- normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []), {:ok, remote_capabilities} <- normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do {:ok, %{ remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]), remote_profile_url: normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]), remote_relay_urls: remote_relay_urls, remote_capabilities: remote_capabilities, requested_capabilities: requested_capabilities, inbound_message: normalize_optional_string(attrs["message"] || attrs[:message]), last_inbound_at: DateTime.utc_now() }} end end defp normalize_observed_relationship_attrs(attrs) do with {:ok, remote_relay_urls} <- normalize_string_list(attrs["relay_urls"] || attrs[:relay_urls] || []), {:ok, remote_capabilities} <- normalize_string_list(attrs["capabilities"] || attrs[:capabilities] || []) do {:ok, %{ remote_api_url: normalize_optional_string(attrs["api_url"] || attrs[:api_url]), remote_profile_url: normalize_optional_string(attrs["profile_url"] || attrs[:profile_url]), remote_relay_urls: remote_relay_urls, remote_capabilities: remote_capabilities }} end end defp normalize_relationship_update(_attrs, nil), do: {:error, :relationship_not_found} defp normalize_relationship_update(attrs, existing) do with {:ok, status} <- normalize_status(attrs["status"] || attrs[:status] || existing.status), {:ok, trust_score} <- normalize_trust_score( attrs["trust_score"] || attrs[:trust_score] || existing.trust_score ), {:ok, granted_capabilities} <- normalize_capability_input( attrs["granted_capabilities"] || attrs[:granted_capabilities] || existing.granted_capabilities || [] ) do {:ok, %{ remote_tribe_pubkey: existing.remote_tribe_pubkey, status: status, remote_api_url: existing.remote_api_url, remote_profile_url: existing.remote_profile_url, remote_relay_urls: existing.remote_relay_urls || [], remote_capabilities: existing.remote_capabilities || [], requested_capabilities: existing.requested_capabilities || [], granted_capabilities: granted_capabilities, trust_score: trust_score, trust_note: normalize_optional_string( attrs["trust_note"] || attrs[:trust_note] || existing.trust_note ), inbound_message: existing.inbound_message, last_inbound_at: existing.last_inbound_at, last_outbound_at: existing.last_outbound_at }} end end defp normalize_capability_input(value) when is_binary(value) do value |> String.split([",", "\n"], trim: true) |> Enum.map(&String.trim/1) |> normalize_string_list() end defp normalize_capability_input(value), do: normalize_string_list(value) defp normalize_pubkey(value) when is_binary(value) do pubkey = value |> String.trim() |> String.downcase() if String.match?(pubkey, ~r/\A[0-9a-f]{64}\z/) do {:ok, pubkey} else {:error, :invalid_pubkey} end end defp normalize_pubkey(_value), do: {:error, :invalid_pubkey} defp normalize_status(:observed), do: {:ok, :observed} defp normalize_status(:pending_inbound), do: {:ok, :pending_inbound} defp normalize_status(:pending_outbound), do: {:ok, :pending_outbound} defp normalize_status(:active), do: {:ok, :active} defp normalize_status(:blocked), do: {:ok, :blocked} defp normalize_status(:revoked), do: {:ok, :revoked} defp normalize_status("observed"), do: {:ok, :observed} defp normalize_status("pending_inbound"), do: {:ok, :pending_inbound} defp normalize_status("pending_outbound"), do: {:ok, :pending_outbound} defp normalize_status("active"), do: {:ok, :active} defp normalize_status("blocked"), do: {:ok, :blocked} defp normalize_status("revoked"), do: {:ok, :revoked} defp normalize_status(_status), do: {:error, :invalid_status} defp normalize_trust_score(value) when is_integer(value) and value >= -100 and value <= 100, do: {:ok, value} defp normalize_trust_score(value) when is_binary(value) do case Integer.parse(String.trim(value)) do {score, ""} -> normalize_trust_score(score) _other -> {:error, :invalid_trust_score} end end defp normalize_trust_score(_value), do: {:error, :invalid_trust_score} defp normalize_string_list(values) when is_list(values) do values |> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} when is_binary(value) -> trimmed = String.trim(value) next = if trimmed == "", do: acc, else: [trimmed | acc] {:cont, {:ok, next}} _value, _acc -> {:halt, {:error, :invalid_string_list}} end) |> case do {:ok, values} -> {:ok, Enum.reverse(values)} {:error, _reason} = error -> error end end defp normalize_string_list(_values), do: {:error, :invalid_string_list} defp normalize_optional_string(nil), do: nil defp normalize_optional_string(value) when is_binary(value) do case String.trim(value) do "" -> nil trimmed -> trimmed end end defp normalize_optional_string(_value), do: nil defp status_rank(:pending_inbound), do: 0 defp status_rank(:pending_outbound), do: 1 defp status_rank(:active), do: 2 defp status_rank(:observed), do: 3 defp status_rank(:blocked), do: 4 defp status_rank(:revoked), do: 5 defp status_rank(_status), do: 6 defp relationship_status(nil), do: "unknown" defp relationship_status(%{status: status}), do: to_string(status) defp relationship_trust_score(nil), do: nil defp relationship_trust_score(%{trust_score: trust_score}), do: trust_score defp relationship_capabilities(nil), do: [] defp relationship_capabilities(%{granted_capabilities: capabilities}), do: capabilities || [] defp default_trust_opts(opts) do Keyword.put_new(opts, :context, %{private: %{system?: true, system_purpose: :trust_plugin}}) end end